Как нечисто изменить состояние, связанное с объектом?

В Haskell у меня есть контейнер вроде:

data Container a = Container { length :: Int, buffer :: Unboxed.Vector (Int,a) }

Этот контейнер представляет собой сплющенное дерево. Его метод доступа (!) выполняет двоичный (log(N)) поиск по вектору, чтобы найти нужное ведро, в котором хранится index.

(!) :: Container a -> Int -> a
container ! index = ... binary search ...

Поскольку последовательные обращения, скорее всего, будут в одном и том же сегменте, это можно оптимизировать следующим образом:

if `index` is on the the last accessed bucket, skip the search

Сложный момент - часть last accessed bucket. В JavaScript я бы просто нечисто изменил скрытую переменную в объекте-контейнере.

function read(index,object){

    var lastBucket = object.__lastBucket;

    // if the last bucket contains index, no need to search
    if (contains(object, lastBucket, index))
        var bucket = lastBucket;

    // if it doesn't
    else {
        // then we search the bucket
        var bucket = searchBucket(index,object);

        // And impurely annotate it on the container, so the
        // next time we access it we could skip the search.
        container.__lastBucket = bucket;
    }

    return object.buffer[bucket].value;
}

Поскольку это просто оптимизация, и результат один и тот же, независимо от выбранной ветки, я считаю, что это не нарушает ссылочную прозрачность. Как возможно в Haskell нечисто изменить состояние, связанное со значением времени выполнения?

~

Я подумал о двух возможных решениях.

  1. Глобальная изменяемая хэш-карта, связывающая указатели со значением lastBucket, и используйте unsafePerformIO для записи в нее. Но мне нужен способ получить указатель времени выполнения объекта или, по крайней мере, какой-то уникальный идентификатор (как?).

  2. Добавьте дополнительное поле в Container, lastBucket :: Int и каким-то образом нечисто измените его в (!) и считайте это поле внутренним (потому что оно явно нарушает ссылочную прозрачность).


person MaiaVictor    schedule 26.08.2015    source источник
comment
Для второго варианта вы можете вместо этого использовать lastBucket :: IORef Int и использовать unsafePerformIO для «нечистой модификации». Ваш код JS не обрабатывает тот факт, что __lastBucket может быть изменен другим вызовом read() из другого потока, следовательно, имеет разные значения в if и =.   -  person Émilien Tlapale    schedule 26.08.2015
comment
@ Xicò, конечно, спасибо! Я хотел бы скрыть это, потому что это не часть API. Кроме того, то, что вы сказали, верно. Но мне здесь не нужны замки, верно? Просто сохраните __lastBucket в переменной в начале. (JS не имеет потоков...). Изменить: обновил OP.   -  person MaiaVictor    schedule 26.08.2015
comment
Остерегайтесь потоков, если вы решите использовать небезопасные приемы.   -  person chi    schedule 26.08.2015
comment
В первом решении с глобальной картой вместо этого потребуется использовать слабую карту, иначе возникнут проблемы со сборкой мусора.   -  person Bergi    schedule 26.08.2015
comment
Поднимите свои операции в ST...   -  person Daniel Wagner    schedule 26.08.2015
comment
Но @DanielWagner там нет фактического состояния в том смысле, что вывод функции не меняется на его основе, это просто внутренняя оптимизация. Разве это не сделает ST API неправильным?   -  person MaiaVictor    schedule 26.08.2015
comment
@Viclib Я не знаю технического определения неправильности. знак равно   -  person Daniel Wagner    schedule 26.08.2015
comment
Я не знаю, что это за английское слово, но я имел в виду что-то дополнительное, не существенное, например пятое колесо для автомобиля... оно не лишнее...   -  person MaiaVictor    schedule 27.08.2015


Ответы (2)


Используя решение (1), мне удалось получить следующий дизайн. Во-первых, я добавил поле __lastAccessedBucket :: IORef Int в свой тип данных, как предложил @Xicò:

data Container a = Container { 
    length :: Int, 
    buffer :: V.Vector (Int,a), 
    __lastAccessedBucket :: IORef Int }

Затем мне пришлось обновить функции, создающие новый Container, чтобы создать новый IORef с использованием unsafePerformIO:

fromList :: [a] -> Container a
fromList list = unsafePerformIO $ do
    ref <- newIORef 0
    return $ Container (L.length list) buffer ref
    where buffer = V.fromList (prepare list)

Наконец, я создал две новые функции: findBucketWithHint, чистую функцию, которая выполняет поиск в корзине индекса с предположением (то есть в корзине, где, по вашему мнению, он может находиться), и функцию unsafeFindBucket, которая заменяет чистую findBucket, когда требуется производительность. всегда используя в качестве подсказки последнее доступное ведро:

unsafeFindBucket :: Int -> Container a -> Int
unsafeFindBucket findIdx container = unsafePerformIO $ do 
    let lastBucketRef = __lastAccessedBucket contianer
    lastBucket       <- readIORef lastBucketRef
    let newBucket     = findBucketWithHint lastBucket findIdx container
    writeIORef lastBucketRef newBucket
    return $ newBucket

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

person MaiaVictor    schedule 26.08.2015
comment
В данном случае это, вероятно, не имеет значения, так как значение представляет собой только кеш, но в целом безопаснее и несколько более идиоматично использовать atomicModifyIORef. - person Petr; 27.08.2015

(Это больше расширенный комментарий, чем ответ.)

Сначала я предлагаю проверить, не является ли это случаем преждевременная оптимизация. В конце концов, O(log n) не так уж и плохо.

Если эта часть действительно критична для производительности, ваше намерение определенно справедливо. Обычное предупреждение для unsafePerformIO: «Используйте его, только если вы знаете, что делаете», что вы, очевидно, делаете, и это может помочь сделать вещи чистыми и быстрыми одновременно. Убедитесь, что вы следуете всем меры предосторожности в документации, в частности установка правильных флагов компилятора (вы можете использовать прагму OPTIONS_GHC).

Также убедитесь, что операция IO является потокобезопасной. Самый простой способ убедиться в этом — использовать IORef вместе с atomicModifyIORef.

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

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

Наконец, вы можете рассмотреть возможность использования отображаемых деревьев вместо массива. Вы по-прежнему будете иметь (амортизированную) сложность O(log n), но их большое преимущество заключается в том, что по замыслу они перемещают часто используемые элементы вверху. Таким образом, если вы будете обращаться к подмножеству элементов размером k, они вскоре будут перемещены наверх, поэтому операции поиска будут всего O(log k) (константа для одного, многократно используемого элемента). Опять же, они обновляют структуру при поиске, но вы можете использовать тот же подход с unsafePerformIO и атомарными обновлениями IORef, чтобы сохранить чистоту внешнего интерфейса.

person Petr    schedule 27.08.2015
comment
Эдвард Кметт работал над сумасшедшими древовидными векторными структурами с множеством небезопасных вещей, которые могли бы хорошо сработать для этого, особенно если также необходимы модификации. - person dfeuer; 28.08.2015
comment
@viclib, вам придется покопаться в GitHub/ekmett и поискать там свежие материалы. Я не уверен, что на самом деле было выпущено еще. - person dfeuer; 02.09.2015