Примеры использования 'signum' для упорядоченных числовых типов в Haskell

signum Функция представляет собой реализацию обычного математического определения знака, который условно возвращает значение в {-1,0,1}. Это теоретическое определение, и, как таковое, оно не учитывает вычислительные затраты на операции или тип данных, поэтому умножение на (-1) является теоретическим способом изменения знака с нулевой стоимостью. Из-за этого это не самая полезная обработка знака для программирования.

Дело signum a == 0 не очень полезно, так как вы всегда можете проверить непосредственно a == 0без дополнительных затрат на вычисления signum a, Что касается двух других значений, я думаю, что они используются только 3 общими способами:

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

    f x y | signum x == -1  = h x y
          | otherwise       = g x y
    
  • или вы умножаете что-то на 1 или же -1 прежде чем работать с ним, как в:

    f x y = g x (y * b) where 
          b = signum x
    
  • или вы добавляете 1 или же -1 к чему-то, прежде чем оперировать им, как в:

    f x y = g x (y + b) where 
          b = signum x
    

Во всех случаях было бы лучше Bool значение для знака. Итак, нам просто нужны функции для разложения Num в абсолютное значение и логический знак, и обратную функцию, которая изменяет знак значения в зависимости от логического условия (которое представляет знак). Эта функция является эквивалентом умножения 1 или же -1 по числу, поэтому мы определяем его как оператор, аналогичный (*),:

sgn  a      = a >= 0
(*.) a True = a
(*.) a _    = -a
abs  a      = a *. sgn a
signum1 a   = 1 *. sgn a

Я добавил дихотомический вариант signum который может только вернуть ´{-1,1}´. Обратите внимание, что перед ним signum 0 = 0 мы бы получили обычный signum функция, но этот третий случай - это то, что, как мне кажется, не очень полезно.

Аналогично мы можем кодировать оператор добавления, потому что это очень частый случай добавления 1 или же -1 в зависимости от знака чего-либо (вы можете видеть, что эти операторы просто обрабатывают True как 1 а также False как -1):

(+.) a b    = a + 1 *. b
(-.) a b    = a - 1 *. b

Мы могли бы даже заключить объявления в класс Signed, для более легкого использования, включая правильные подписи и исправления.

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

Мой вопрос, таким образом, заключается в том, есть ли случаи, отличные от трех общих случаев использования, раскрытых здесь, то есть случаи, которые нелегко охватить этим подходом, случаи, для которых текущий signum Функция выгодна против подхода Bool. Точнее, могу ли я полностью избежать использования текущего signum функционировать без потери эффективности или ясности кода?


Редактировать: я изменил первый абзац на более "нейтральный" способ, следуя комментарию Рида Бартона.


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

3 ответа

Решение

Я использовал такую ​​функцию, чтобы перемещать курсор к целевому смещению в квадратной сетке с помощью ряда ходов (ортогонально и по диагонали) соседних ячеек.

move :: (Int, Int) -> [(Int, Int)]
move (0, 0) = []
move (x, y) = (dx, dy) : move (x - dx, y - dy)
  where dx = signum x
        dy = signum y

Вы предполагаете, что "положительный" и "отрицательный" являются единственными двумя возможными признаками. Но например Complex Double, signum Операция возвращает комплексное число с тем же "направлением", но величиной 1:

Data.Complex> signum (3 :+ 4)
0.6 :+ 0.8

Для решения вопроса о сложности времени:

Ветви не являются свободными, и если вам (в концепции) нужно умножить значения на результат значения одного и того же значения в нескольких разных точках, вероятно, было бы более эффективным иметь let s = signum x in ... или иметь эту привязку в where-clause. Вам больше не нужно проходить через ветку каждый раз. Также имейте в виду, что в некоторых случаях код может замедляться из-за поведения ветвления, которое идет вразрез с ожидаемым предсказателем ветвления.

Например, скажем, у вас есть такой код:

f x y z = (s * y) / (1 + (s * z))
  where
    s = signum x

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

Другие вопросы по тегам