Проблема с заменой битов в Haskell

В рамках школьного проекта я реализую некоторые криптографические алгоритмы на Haskell. Как вы, наверное, знаете, это включает в себя довольно много низкоуровневых битов. Теперь я застрял на одной конкретной подпрограмме, которая вызывает у меня головную боль. Подпрограмма, представляющая собой перестановку на 256 бит, работает следующим образом:

Вход: 256-битный блок.
Тогда все четные биты (0,2,...) во входном блоке считаются первыми 128 битами в выходном блоке. В то время как нечетные биты считаются последними 128 битами в выходном блоке. В частности, формула для i-го бита на выходе задается как (ai — это i-й бит на входе блок, а b — выход):

бi = а2i

bi+2d-1 = a2i + 1

для i от 0 до 2d-1-1, d = 8.

В качестве игрушечного примера предположим, что мы использовали сокращенную версию подпрограммы, которая работала с 16-битными блоками вместо 256-битных. Тогда следующая битовая строка будет переставлена ​​следующим образом:

1010 1010 1010 1010 -> 1111 1111 0000 0000

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

Я буду очень признателен за любой намек или совет о том, как подойти к этой проблеме.


person hakoja    schedule 04.09.2011    source источник
comment


Ответы (3)


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

import Data.ByteString (pack, unpack, ByteString)
import Data.Bits
import Data.Word

-- the main attraction
packString :: ByteString -> ByteString
packString = pack . packWords . unpack

-- main attraction equivalent, in [Word8]
packWords :: [Word8] -> [Word8]
packWords ws = evenPacked ++ unevenPacked
    where evenBits = map packEven ws
          unevenBits = map packUneven ws
          evenPacked = consumePairs packNibbles evenBits
          unevenPacked = consumePairs packNibbles unevenBits

-- combines 2 low nibbles (first 4 bytes) into a (high nibble, low nibble) word
-- assumes that only the low nibble of both arguments can be non-zero. 
packNibbles :: Word8 -> Word8 -> Word8
packNibbles w1 w2 = (shiftL w1 4) .|. w2 

packEven w = packBits w [0, 2, 4, 6]

packUneven w = packBits w [1, 3, 5, 7]

-- packBits 254 [0, 2, 4, 6] = 14 
-- packBits 254 [1, 3, 5, 7] = 15
packBits :: Word8 -> [Int] -> Word8
packBits w is = foldr (.|.) 0 $ map (packBit w) is

-- packBit 255 0 = 1
-- packBit 255 1 = 1
-- packBit 255 2 = 2
-- packBit 255 3 = 2
-- packBit 255 4 = 4
-- packBit 255 5 = 4
-- packBit 255 6 = 8
-- packBit 255 7 = 8
packBit :: Word8 -> Int -> Word8
packBit w i = shiftR (w .&. 2^i) ((i `div` 2) + (i `mod` 2))

-- sort of like map, but halves the list in size by consuming two elements. 
-- Is there a clearer way to write this with built-in function?
consumePairs :: (a -> a -> b) -> [a] -> [b]
consumePairs f (x : x' : xs) = f x x' : consumePairs f xs
consumePairs _ [] = []
consumePairs _ _ = error "list must contain even number of elements"
person Boris    schedule 04.09.2011
comment
Хороший! Это хорошее решение. Вы позволите мне включить это в мой алгоритм? Я заметил, что вы поменяли местами порядок битов (т.е. 1111 1111 0000 0000 превращается в 0000 1111 0000 1111, а не в 1111 0000 1111 0000), но это быстрое решение. Я согласен с тем, что решение Криса станет действительно хорошей тестовой моделью для быстрой проверки. - person hakoja; 04.09.2011
comment
@hakoja: исправлено. Я не думал ясно. Каким-то образом я понял, что младший бит первого байта будет первым битом в последовательности, поэтому я интерпретировал 10101010 как 85 вместо 170. - person Boris; 04.09.2011
comment
В нижнем колонтитуле этой страницы есть уведомление о том, что пользовательский контент распространяется под лицензией Creative Лицензия Commons Attribution-ShareAlike 3.0 Unported означает, что вы можете свободно использовать (частно и в коммерческих целях), делиться контентом и делать ремиксы на следующих условиях: если вы используете произведение, вы должны указать автора, и если любая производная работа выпускается, она должна быть под аналогичной лицензией. Настоящим я отказываюсь от этих условий, поэтому вы можете использовать код по своему усмотрению. - person Boris; 04.09.2011
comment
Извините, но я думаю, что это я перепутал MSB/LSB в моем примере выше по сравнению с представлением в Haskell. Похоже, ваша первоначальная версия была абсолютно правильной :) Думаю, я слишком много перетасовал в голове за последний день. Но теперь я слишком запутался, чтобы быть в чем-то уверенным :D - person hakoja; 04.09.2011
comment
@hakoja: Если вы используете код, вам следует учитывать любой кодекс чести, который может использовать ваша школа. имеют. В этом видео обсуждается кодекс чести Стэнфорда, начало примерно в 34:30. - person Boris; 04.09.2011
comment
Спасибо за вашу помощь, я очень ценю это. Что касается вашего кода, я, конечно, не буду использовать его как есть, но я надеюсь, что смогу использовать некоторые из лежащих в его основе алгоритмических идей при разработке моей собственной версии. Я с радостью отдаю вам должное за эту работу. С этой целью мне понадобится ваше полное имя, если вы хотите, чтобы это было указано, а не только ваш онлайн-профиль. - person hakoja; 04.09.2011
comment
давайте продолжим это обсуждение в чате - person hakoja; 04.09.2011
comment
@хакоя. На самом деле, я все еще думаю, что ошибался. Я сел и проработал ваш 16-битный пример из комментария ручкой и бумагой, и я думаю, что пример правильный, согласно спецификации, независимо от того, присвоен ли левому биту в последовательности индекс 0 или 15. И когда вы интерпретируете 8 битов как байт, причем крайний левый является MSB, тогда packWords [255, 0] должен быть [240, 240], что соответствует 1111 0000 1111 0000. Это то, что код дает сейчас, тогда как раньше он давал [15, 15]. - person Boris; 04.09.2011

это должно работать:

import Data.List
import Data.Function

map fst $ sortBy (compare `on` snd) $ zip yourList $ cycle [0,1]

Небольшое пояснение: поскольку sortBy сохраняет исходный порядок, мы можем соединить каждое значение в четной позиции с «0» и каждое значение в нечетной позиции с «1», затем мы просто сортируем по второму значению пары. Таким образом, все значения в четных позициях будут помещены перед значениями в нечетных позициях, но их порядок будет сохранен.

Крис

person Chris    schedule 04.09.2011
comment
Эта функция предполагает, что вы работаете со списком битовых значений, что может быть непрактичным из-за требуемого размера (по крайней мере, Word8 для хранения каждого бита) и скорости (вероятно, намного медленнее, чем битовые операции). - person John L; 04.09.2011
comment
@Chris: Спасибо за ваш ответ, он действительно умный. Тем не менее, я не понимаю, как я могу преобразовать ByteString в список «1» и «0» с эффективным использованием времени и пространства (как упоминалось Джоном Л.). Я чувствую, что инструментов для работы на битовом уровне в несколько байтов одновременно в Haskell несколько не хватает. Я знаю, что модуль Data.Bits связан с дальнейшим, но я не предоставляю именно то, что мне нужно в этом случае. Я постараюсь выяснить и некоторые другие подходы и посмотреть, как они сравниваются с вашим решением. Может быть, распакованный массив Word8 поможет? - person hakoja; 04.09.2011
comment
Мне это нравится. Это если и не очень эффективно, то совершенно очевидно правильно. В зависимости от приложения это может не иметь значения, если для вычисления требуется больше времени. Даже если вам нужно больше эффективности, это действительно хорошая модель для быстрой проверки. - person Boris; 04.09.2011

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

import Data.Bits
import qualified Data.Vector as V

type BitVector = V.Vector Bool

unpack :: (Bits a) => a -> BitVector
unpack w = V.generate (bitSize w) (testBit w)

pack :: (Bits a) => BitVector -> a
pack v = V.ifoldl' set 0 v
  where
    set w i True = w `setBit` i
    set w _ _    = w

mkPermutationVector :: Int -> V.Vector Int
mkPermutationVector d = V.generate (2^d) b
  where
    b i | i < 2^(d-1) = 2*i
        | otherwise   = let i' = i-2^(d-1)
                        in 2*i'+1

permute :: Int -> BitVector -> BitVector
permute d v = V.backpermute v (mkPermutationVector d)

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

Чтобы проверить ваш пример вектора (в базе 10):

*Main> import Data.Word
*Main Data.Word> let permute16 = pack . permute 4 . unpack :: Word16 -> Word16
*Main Data.Word> permute16 43690
65280

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

plus :: BitVector -> BitVector -> BitVector
plus as bs = V.tail sums
  where
    (sums, carries) = V.unzip sumsAndCarries
    sumsAndCarries  = V.scanl' fullAdd (False, False) (V.zip as bs)
    fullAdd (_, cin) (a, b) = ((a /= b) /= cin
                              , (a && b) || (cin && (a /= b)))

Вы также можете найти полезным пакет Левента Эркока sbv, хотя я не уверен, что он предоставляет функцию как удобно как backpermute для вашего конкретного вопроса.

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

person acfoltzer    schedule 04.09.2011
comment
Я никогда раньше не смотрел на Data.Vector, но из-за обратной перестановки это решение выглядит действительно хорошо. - person Boris; 04.09.2011