Рубрики
Без рубрики

Позвольте компилятору сделать всю работу за вас!

Недавно я наткнулся на небольшую программную головоломку, задача состояла в том, чтобы взять двоичное дерево поиска и повторить… Помеченный как java, haskell, функциональный.

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

    5
   / \
  2    7
 /    / \
1    6   8
      \
       6

Мы начинаем с крайнего правого числа и устанавливаем его равным нулю (потому что сумма ничего по-прежнему равна нулю). Семерка над ним идет следующей, и текущая сумма равна 8, потому что справа есть только один узел. После этого следует “второй” 6, то есть дочерний элемент остальных 6, опять же, потому что он находится дальше вправо. В общем случае порядок всегда таков: правое поддерево, self, левое поддерево. В конце концов мы получаем это дерево, проверьте, понимаете ли вы, откуда берется каждое число:

    27
   /  \
  32   8
 /    / \
34   21  0
      \
       15

Для следующих реализаций мы будем использовать только материалы из стандартной библиотеки.

Во-первых, мы определяем себя как дерево:

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

    public Node(Node l, int v, Node r) {
        this.left = l;
        this.value = v;
        this.right = r;
    }
}

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

public class Pair {
    Node n;
    int v;

    public Pair(Node n, int v) {
        this.n = n;
        this.v = v;
    }
}

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

    public Node(Node l, int v, Node r) { /* ... */ }

    public Pair solve(int currentSum) {
        // Store the current sum in here, as fallback if right is null
        Pair rightResult = new Pair(null, currentSum);
        // First, go to the right subtree, if it exists
        if(this.right != null) {
            rightResult = this.right.solve(currentSum);
        }
        int sum = rightResult.v + this.value;

        // Again, save the sum as fallback
        Pair leftResult = new Pair(null, sum);
        if(this.left != null) {
            leftResult = this.left.solve(sum);
        }

        // Finally create a new node (to replace self)
        Node newSelf = new Node(leftResult.n, rightResult.v, rightResult.n);
       // And return it together with the sum
       return new Pair(newSelf, leftResult.v);
    }
}    

Теперь нам просто нужна основная функция для запуска этого:

public class Main {
    static Node testTree = new Node(
        new Node(
            new Node(null, 1, null),
            2,
            null
        ),
        5,
        new Node(
            new Node(
                null,
                6,
                new Node(null, 6, null)
            ),
            7,
            new Node(null, 8, null)
        )
    );

    public static void main(String[] args) {
        Pair result = testTree.solve(0);
        System.out.println(result.n);
    }
}

Чтобы увидеть наш результат, нам также нужен метод toString на узле:

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

    public Node(Node l, int v, Node r) { /* ... */ }

    public Pair solve(int currentSum) { /* ... */ }

    public String toString() {
        String leftTree = left == null ? " " : left.toString();
        String rightTree = right == null ? " " : right.toString();
        return "Node(" + leftTree + ", " + value + ", " + rightTree + ")";
    }
}

Если вы запустите код сейчас, вы увидите

Node(Node(Node( , 34,  ), 32,  ), 27, Node(Node( , 21, Node( , 15,  )), 8, Node( , 0,  )))

это именно то дерево, которое мы ищем!

Теперь вы можете спросить, какое это имеет отношение к названию? Компилятор не пишет код для нас здесь. Это происходит потому, что компилятор Java – это просто “проверяющий” компилятор (как я его называю). Он жалуется, если вы не соответствуете типам или допускаете синтаксические ошибки, но он не работает для вас, он в основном похож на учителя, который впоследствии проверяет ваш ответ.

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

Одной из таких фундаментальных абстракций является Функтор . Если ваш тип данных является функтором, вы можете сопоставить над ним функцию. Ни больше, ни меньше. Например, массив или список – это функтор, в котором вы применяете функцию для каждого элемента (вы можете знать это из Java streams или JavaScript Array.map). В Haskell это работает с любым типом данных, который “содержит” другие данные. Итак, давайте определим такой тип данных – обратите внимание, что мы оставляем тип данных в дереве абстрактным, в примере Java это было интересно :

module Main where

data Tree a = Leaf | Node (Tree a) a (Tree a)

Это объявление в основном совпадает с объявлением Node class в примере Java. Просто вместо null мы используем Leaf для сигнализации пустых дочерних элементов, и мы не называли данные left , значение , право правильно , но просто расположите их в таком порядке.

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

{-# LANGUAGE DeriveFunctor #-}
module Main where

data Tree a = Leaf | Node (Tree a) a (Tree a)
            deriving Functor

правильно || , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение || deriving ||. Это уже позволило бы нам увеличить все узлы в дереве на единицу, например:

incrementNodes :: Tree Int -> Tree Int
incrementNodes tree = fmap (+1) tree

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

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
module Main where

data Tree a = Leaf | Node (Tree a) a (Tree a)
            deriving (Functor, Foldable)

правильно || , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение || deriving ||. Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте для сворачивания структуры до единственного значения, есть thAgain, массив и списки складываются, и это снова работает так же, как collecAnd, это может быть автоматически сгенерировано с помощью CONOW, мы могли бы вычислить сумму всех узлов, например: mpiler для вас: t в Java 8 и Array.reduce в JavaScript. at требует, чтобы ваши данные уже были || Функтором ||. e складной || класс типа e || o вторая половина.

sumNodes :: Tree Int -> Int
sumNodes tree = foldr (+) 0 tree

правильно , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение deriving . Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте для свертывания структуры до единственного значения, есть еще один, массив и списки складываются, и это работает так же, как collecand, опять же, это может быть автоматически сгенерировано с помощью CONOW, мы могли бы вычислить сумму всех узлов, например: mpileNow к последнему шагу: сопоставление и сворачивание в одном. r для вас: t в Java 8 и Array.reduce в JavaScript. at требует, чтобы ваши данные уже были Функтором

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
module Main where

data Tree a = Leaf | Node (Tree a) a (Tree a)
            deriving (Functor, Foldable, Traversable)

правильно || , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение || deriving ||. Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте свернем структуру до единственного значения, есть thagain, массив и списки складываются, и это работает точно так же, как colleCand, снова, это может быть автоматически сгенерировано CONOW, мы могли бы вычислить сумму всех узлов, например: MPIL Со всем этим на месте, наше окончательное решение довольно простое: И, как и предыдущие классы, компилятор может автоматически сгенерировать его для нас: например, мы можем отслеживать некоторое локальное состояние – текущую сумму. Этот класс позволяет сопоставлять, а также одновременно отслеживать некоторые “побочные эффекты”, но для этого необходимо, чтобы ваши данные уже были || складными ||. Проходимый || . Для этого нам нужна еще одна абстракция – || Теперь перейдем к последнему шагу: сопоставлению и сворачиванию в одно целое. r для вас: t в Java 8 и Array.reduce в JavaScript. at требует, чтобы ваши данные уже были || Функтором ||. e складной || класс типа e || o вторая половина.

solve :: Tree Int -> (Int, Tree Int)
solve tree = mapAccumR (\a b -> (a + b, a)) 0 tree

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

mapAccumR :: Traversable t => (a -> b -> (a, c)) -> a -> t b -> (a, t c)
mapAccumR fun init x = runStateR (traverse (StateR . flip fun) x) init

правильно , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение deriving . Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте свернем структуру до единственного значения, есть еще один параметр, массив и списки складываются, и это работает так же, как colleCand, опять же, это может быть автоматически сгенерировано CONOW, мы могли бы вычислить сумму всех узлов, например: mpilэто точное определение не так важно, просто посмотрите, что во внутренней скобке он использует STATER , чтобы ваша функция вела себя как побочный эффект. mapAccumR – это функция из стандартной библиотеки Haskell, которая определяется следующим образом: При наличии всего этого наше окончательное решение довольно простое: И, как и предыдущие классы, компилятор может автоматически сгенерировать его для нас: например, мы можем отслеживать некоторое локальное состояние – текущую сумму. Этот класс позволяет сопоставлять, а также одновременно отслеживать некоторые “побочные эффекты”, но для этого необходимо, чтобы ваши данные уже были

правильно , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение deriving . Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте свернем структуру до единственного значения, есть thagain, массив и списки складываются, и это снова работает так же, как colleCand, это может быть автоматически сгенерировано с помощью CONOW, мы могли бы вычислить сумму всех узлов, например: mpileJava потребовал, чтобы мы написали метод toString

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
module Main where

data Tree a = Leaf | Node (Tree a) a (Tree a)
            deriving (Show, Functor, Foldable, Traversable)

solve :: Tree Int -> (Int, Tree Int)
solve tree = mapAccumR (\a b -> (a + b, a)) 0 tree

testTree :: Tree Int
testTree =
    Node
        (Node
            (Node Leaf 1 Leaf)
            2
            Leaf
        )
        5
        (Node
            (Node
                Leaf
                6
                (Node Leaf 6 Leaf)
            )
            7
            (Node Leaf 8 Leaf)
        )

main :: IO ()
main = do
    let (sum, tree) = solve testTree
    print tree

правильно || , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение || deriving ||. Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте свернем структуру до единственного значения, есть thAgain, массив и списки складываются, и это снова работает так же, как collecAnd, это может быть автоматически сгенерировано с помощью CONOW, мы могли бы вычислить сумму всех узлов, например: mpileAs вы можете увидеть, поддерживает ли вас компилятор, вы можете сильно сократить количество строк кода. Итак, с помощью основного метода, позволяющего сделать все работоспособным, наш полный код для головоломки – это просто: Show || . В Haskell мы снова можем использовать компилятор для этого, выведя || Java потребовала, чтобы мы написали метод || toString || вручную. Затем он использует || traverse || для объединения эффектов с сохранением состояния в правильном порядке и || runStateR || затем выполняет их один за другим. Это точное определение не так важно, просто посмотрите, что во внутренней круглой скобке используется || StateR ||, чтобы ваша функция вела себя как побочный эффект. mapAccumR || – это функция из стандартной библиотеки Haskell, которая определяется следующим образом: При наличии всего этого наше окончательное решение довольно простое: И, как и предыдущие классы, компилятор может автоматически сгенерировать его для нас: например, мы можем отслеживать некоторое локальное состояние – текущую сумму. Этот класс позволяет сопоставлять, а также одновременно отслеживать некоторые “побочные эффекты”, но для этого необходимо, чтобы ваши данные уже были || складными ||. Проходимый || . Для этого нам нужна еще одна абстракция – || Теперь перейдем к последнему шагу: сопоставлению и сворачиванию в одно целое. r для вас: t в Java 8 и Array.reduce в JavaScript. at требует, чтобы ваши данные уже были || Функтором ||. e складной || класс типа e || o вторая половина. правильно || , но просто расположите их в таком порядке. Итак, теперь вернемся к функтору: компилятор способен автоматически генерировать код, который вам понадобится для реализации этого сопоставления. Все, что вам нужно сделать, это включить эту функцию и использовать предложение || deriving ||. Это уже позволило бы нам увеличить все узлы в дереве, например, на единицу: Но теперь мы хотели бы сопоставлять и собирать одновременно. Мы решили первую половину, давайте свернем структуру до единственного значения, есть thagain, массив и списки складываются, и это снова работает так же, как colleCand, это может быть автоматически сгенерировано с помощью CONOW, мы могли бы вычислить сумму всех узлов, например: mpileMore код всегда означает больше ошибок, поэтому все, что нам не нужно писать самим, хорошо. Как вы можете видеть, если компилятор вас поддержит, вы можете значительно сократить количество строк кода. Итак, с помощью основного метода, позволяющего сделать все работоспособным, наш полный код для головоломки – это просто: Show || . В Haskell мы снова можем использовать компилятор для этого, выведя || Java потребовала, чтобы мы написали метод || toString || вручную. Затем он использует || traverse || для объединения эффектов с сохранением состояния в правильном порядке и || runStateR || затем выполняет их один за другим. Это точное определение не так важно, просто посмотрите, что во внутренней круглой скобке используется || StateR ||, чтобы ваша функция вела себя как побочный эффект. mapAccumR || – это функция из стандартной библиотеки Haskell, которая определяется следующим образом: При наличии всего этого наше окончательное решение довольно простое: И, как и предыдущие классы, компилятор может автоматически сгенерировать его для нас: например, мы можем отслеживать некоторое локальное состояние – текущую сумму. Этот класс позволяет сопоставлять, а также одновременно отслеживать некоторые “побочные эффекты”, но для этого необходимо, чтобы ваши данные уже были || складными ||. Проходимый || . Для этого нам нужна еще одна абстракция – || Теперь перейдем к последнему шагу: сопоставлению и сворачиванию в одно целое. r для вас: t в Java 8 и Array.reduce в JavaScript. at требует, чтобы ваши данные уже были || Функтором ||. e складной || класс типа e || o вторая половина.

Оригинал: “https://dev.to/jvanbruegge/let-the-compiler-do-the-work-for-you-22kl”