Как реализовать двоичные числа в Haskell

Я видел следующий конструктор данных для цифр Чёрча

data Nat = Zero | Succ Nat deriving Show

Но это унарные числа. Как реализовать конструктор данных для двоичных чисел в Haskell таким образом?

Я пробовал это:

data Bin = Zero | One | BinC [Bin] deriving Show

После этого мы можем получить десятичную цифру 5, закодированную как BinC [One,Zero,One]

Но мне кажется, что я что-то здесь упускаю. Мое решение кажется не таким умным, как решение Церкви. Неудивительно, я не Черч. Немного подумав, я обнаружил, что мое решение зависит от списка, тогда как Nat не зависит от такой внешней структуры, как список.

Можем ли мы написать решение, похожее на решение Черча, использующее конструктор типа Succ и для двоичных чисел? Если да, то как? Я много пробовал, но, похоже, мой мозг не может избавиться от необходимости списка или какой-либо другой подобной структуры.


person Tem Pora    schedule 21.08.2013    source источник
comment
В главе 9 книги Криса Окасаки «Чисто функциональные структуры данных» рассматриваются числовые представления с акцентом на двоичные числа. Может быть, вы можете получить достаточно хороший предварительный просмотр книги с помощью Google Книг, чтобы найти некоторые указатели на связанную работу.   -  person stephen tetley    schedule 21.08.2013


Ответы (3)


Самое близкое, о чем я могу думать, было бы чем-то вроде

λ> data Bin = LSB | Zero Bin | One Bin
λ|  -- deriving Show

Это позволяет строить двоичные числа, выполняя только

λ> One . One . Zero . Zero . One . One $ LSB
One (One (Zero (Zero (One (One LSB)))))

Можно также представить функцию декодирования, работающую по принципу (намного лучшая версия, предложенная Инго в комментариях)

λ> let toInt :: (Integral a) => Bin -> a
λ|     toInt = flip decode 0
λ|       where decode :: (Integral a) => Bin -> a -> a
λ|             decode LSB value = value
λ|             decode (Zero rest) value = decode rest (2*value)
λ|             decode (One rest) value = decode rest (2*value + 1)

Который затем можно использовать для декодирования двоичного числа в целое число.

λ> toInt (Zero . One . One . One . Zero . Zero . One $ LSB)
57

Трудность с тем, чего вы хотите достичь, заключается в том, что вам нужно читать двоичные числа «наизнанку» или, так сказать. Чтобы узнать значение старшей значащей цифры, нужно знать, сколько цифр у вас в числе. Если бы вы записали свои двоичные числа в «обратном» порядке, то есть самая внешняя цифра является наименее значащей цифрой, тогда с этим было бы намного проще справиться, но числа смотрели бы назад, когда вы их создаете и распечатываете, используя экземпляр по умолчанию. из Show.

Причина, по которой это не проблема с унарными числами, заключается в том, что не существует «наименее значащей цифры», поскольку все цифры имеют одинаковое значение, поэтому вы можете анализировать число в любом направлении, и вы получите тот же результат.


Для полноты, вот то же самое, но с самой дальней цифрой, являющейся младшей значащей цифрой:

λ> data Bin = MSB | Zero Bin | One Bin
λ|   -- deriving Show

Это выглядит почти так же, как и раньше, но вы заметите, что когда реализована функция декодирования,

λ> let toInt = flip decode (1,0)
λ|       where
λ|         decode (One rest) (pos, val) = decode rest (pos*2, val+pos)
λ|         decode (Zero rest) (pos, val) = decode rest (pos*2, val)
λ|         decode MSB (_, val) = val

Цифры пишутся наоборот!

λ> toInt (Zero . Zero . Zero . One . Zero . One $ MSB)
40

Однако справиться с этим намного проще. Например, мы можем добавить два двоичных числа в каждом конкретном случае. (Внимание: много случаев!)

λ> let add a b = addWithCarry a b False
λ|      where
λ|        addWithCarry :: Bin -> Bin -> Bool -> Bin
λ|        addWithCarry MSB MSB True = One MSB
λ|        addWithCarry MSB MSB False = MSB
λ|        addWithCarry MSB b c = addWithCarry (Zero MSB) b c
λ|        addWithCarry a MSB c = addWithCarry a (Zero MSB) c
λ|        addWithCarry (Zero restA) (Zero restB) False = Zero (addWithCarry restA restB False)
λ|        addWithCarry (One restA)  (Zero restB) False = One (addWithCarry restA restB False)
λ|        addWithCarry (Zero restA) (One restB)  False = One (addWithCarry restA restB False)
λ|        addWithCarry (One restA)  (One restB)  False = Zero (addWithCarry restA restB True)
λ|        addWithCarry (Zero restA) (Zero restB) True = One (addWithCarry restA restB False)
λ|        addWithCarry (One restA)  (Zero restB) True = Zero (addWithCarry restA restB True)
λ|        addWithCarry (Zero restA) (One restB)  True = Zero (addWithCarry restA restB True)
λ|        addWithCarry (One restA)  (One restB)  True = One (addWithCarry restA restB True)

В этот момент добавить два двоичных числа очень просто:

λ> let forty = Zero . Zero . Zero . One . Zero . One $ MSB
λ|     eight = Zero . Zero . Zero . One $ MSB
λ|
λ> add forty eight
Zero (Zero (Zero (Zero (One (One MSB)))))

И действительно!

λ> toInt $ Zero (Zero (Zero (Zero (One (One MSB)))))
48
person kqr    schedule 21.08.2013
comment
Это хорошо работает для натуральных чисел в двоичной форме. Если вам нужны целые числа, у вас может быть два конструктора End, давайте назовем их Zeroes и Ones. Они соответствуют остальным битам, равным 0 или 1. Таким образом, вы получаете целые числа в форме 2-дополнения. Итак, 2 будет Zero $ One $ Zeros, а -2 будет Zero $ Ones. - person augustss; 21.08.2013
comment
Увеличение значимости цифр в направлении, в котором вы читаете, — это то, как изначально делались числа. Просто когда западный мир брал числа у арабов/индейцев, мы не меняли местами цифры в числах, хотя читаем в противоположном направлении, чем они. - person augustss; 21.08.2013
comment
@augustss Но мы пишем большие числа римскими цифрами (например, MCMXLVII — 1947) справа налево. Значит ли это, что мы сохранили арабские числа справа налево по привычке или просто потому, что об этом никто не подумал? - person kqr; 21.08.2013
comment
Спасибо за хороший ответ. но я попробовал это, toInt $ add forty forty, я получил ответ 16. и toInt $ add eight eight дал 0. - person Tem Pora; 21.08.2013
comment
Ага. Это ошибка в функции добавления, связанная с цифрой переноса. Поскольку я сижу на своем телефоне, я оставлю это в качестве упражнения, чтобы исправить, пока я не вернусь домой! - person kqr; 21.08.2013
comment
Я не понимаю, почему функция toInt не может быть намного проще, чем toInt x = go x 0 where go LSB v = v;go (Zero x) v = go x (2*v); go (One x) v = go x (2*v+1) - person Ingo; 21.08.2013
comment
@Ingo Потому что я не самый острый инструмент в сарае. Конечно, это намного, намного лучшая версия. Вероятно, вы могли бы провести аналогичную оптимизацию и для других операций, сделав все мои обратные числа гораздо более бесполезными аргументами? - person kqr; 21.08.2013
comment
@kqr - я думаю, что внешний LSB все же лучше. Например, четные/нечетные равны O(1). Кроме того, таким образом можно лучше выполнять битовую арифметику и арифметические операции, такие как сложение. inc и dec также равны O(1) в 50% всех случаев и лишь немного хуже для четных чисел. - person Ingo; 21.08.2013
comment
Действительно хороший и полный ответ для меня на этот вопрос. Я закончил часть, которую вы оставили неисправленной. Это было для меня наслаждением. Я не могу представить, как вы пришли к идее LSB. Я допускаю, что мне не могла прийти в голову такая идея. - person Tem Pora; 22.08.2013

Просто дополнение к другим ответам, которые вы получили:

Значения данных, которые вы создаете, на самом деле являются числами Пеано, а не числами Черча. Они тесно связаны, но на самом деле они двойственны/обратны друг другу. Числительные Пеано построены на идее построения чисел из концепции множества, для представления которой в Haskell мы используем тесно связанную концепцию типа данных.

{-# LANGUAGE RankNTypes #-}

import Prelude hiding (succ)

data Peano = Zero
           | Succ Peano
  deriving (Show)

Цифры Черча, с другой стороны, кодируют цифры как функции:

type Church = forall n. (n -> n) -> n -> n

zero :: Church
zero = \p -> id

succ :: Church -> Church
succ = \n p -> p . n p

Теперь вы можете собрать их вместе:

peano :: Church -> Peano
peano c = c Succ Zero

fold :: forall n. (n -> n) -> n -> Peano -> n
fold s z Zero     = z
fold s z (Succ p) = s (fold s z p)

church :: Peano -> Church
church p = \s z -> fold s z p

Таким образом, цифры Чёрча, по сути, являются сгибом над цифрами Пеано! И (peano . church) - это идентификатор для чисел Пеано, хотя с типами, указанными выше, Haskell не позволит вам напрямую составить их таким образом. Если вы пропустите объявления типов, Haskell выведет достаточно общие типы, чтобы вы могли их составить.

Хороший обзор различий и их связи друг с другом в контексте функционального программирования можно найти здесь, в Theoretical Pearl Ральфа Хинце Церковные цифры, дважды!.

Вы можете еще больше обобщить эту двойственность; цифры Пеано, по сути, являются начальной F-алгеброй для натуральных чисел, а цифры Черча, по сути, являются конечной / конечной F-коалгеброй для натуральных чисел. Хорошим введением в это является Учебное пособие по (Co)алгебрам и (Co) алгебрам Барта Джейкобса и Яна Руттена. Индукция.

person Levi Pearson    schedule 21.08.2013
comment
Большое спасибо за подробное объяснение этой двойной вещи. Это совершенно новое для меня. Учебник тоже выглядит довольно интересно. - person Tem Pora; 22.08.2013

person    schedule
comment
Спасибо. Но зачем вам два конструктора? Разве нельзя сделать это только один? - person Tem Pora; 21.08.2013
comment
Что интересно, это в основном менее общий связанный список — именно то, что OP сделал в своей первой реализации. - person kqr; 21.08.2013
comment
Для кодирования битов Zero и One нам нужен отдельный конструктор, в то время как у Nat нет такого требования - person Ankur; 21.08.2013
comment
@kqr: Да, на самом деле в типе OP мы можем даже сделать BinC [BinC [One,Zero,One],Zero,One], чего мы не хотим. - person Ankur; 21.08.2013
comment
Как насчет использования newtype Bin = Bin [Bit] вместо объявления данных? (Bin [] можно считать равным 0) - person Nicolas; 21.08.2013
comment
@Nicolas: ОП сказал: I found that my solution depends upon list, whereas the Nat doesn't depend upon any such external structure like list - person Ankur; 21.08.2013