Подтвердить что ты не робот

Строго, возможно, в определениях данных

Я видел много разговоров/читал сообщения в блогах, что у вас должны быть строгие поля в data, чтобы избежать различных проблем с производительностью, например:

data Person = Person
    { personName     :: !Text
    , personBirthday :: !UTCTime
    }

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

Но если я добавлю поле Maybe:

data Person = Person
    { personName     :: !Text
    , personBirthday :: !UTCTime
    , personAddress  :: !(Maybe Address)
    }

Я вношу ленивость в структуру данных, ведь Maybe - это структура управления. Не удается ли ununaluated thunk скрываться за конструктором Just?

Тем не менее, strict Maybe в strict или через strict-base-types. Но в соответствии с обратными зависимостями (strict, strict-base-types) они широко не используются.

Итак, вопрос: почему нужно или не следует использовать строгие Maybe в определениях неконтролируемых данных?

4b9b3361

Ответ 1

Причины использования строгих типов /Maybe/Tuple:

  • Если вы просматриваете свой код и замечаете утечку пространства, это может быть способ подключить утечку

  • Строгие типы данных широко рассматриваются как полезная вещь для высокопроизводительного кода даже с помощью последних расширений языка GHC 8.0

  • Другие люди делают это тоже (эти строгие пакеты могут быть не популярны, но они существуют по какой-либо причине - вы также можете утверждать, что такие приложения, где вам нужны эти строгие пакеты, вероятно, не будут загружены к Hackage)

Причины нет:

  • Не в Prelude, так что это дополнительный пакет

  • Вероятно, вы не пишете высокопроизводительный код

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

  • Если вы пишете высокопроизводительный код, вы могли бы принудительно провести оценку на thunk внутри Maybe в любом случае

В целом я не думаю, что догма идет так или иначе. Это просто вопрос удобства.

Ответ 2

Это не так просто, как "строгий быстро, ленивый медленно".

Обе лень и строгость полезны для повышения производительности; посмотрим, как может быть лень:

  • Очевидно, что sum $ take 10 $ [1..] принимает бесконечное время и бесконечную память, если список является строгим, но конечное время и постоянная память, если список ленив.

  • Структуры функциональных данных обычно не допускают хороших амортизационных ограничений. Что такое "амортизация"? Когда вы оплачиваете стоимость O (f (n)) только после O (n) других полностью несвязанных шагов, мы можем вообразить ее переосмысление как выплату O (f (n)/n) для каждого из этих шагов: так что если вы добавьте, скажем, n элементов в список, а затем отсортируйте их один раз в n логарифмическом времени, затем вы можете переосмыслить как каждое добавление, регистрирующее log n time. (Что он делает, если вы вернете его с помощью самобалансирующегося двоичного дерева поиска вместо списка, но мы можем сказать, что даже со списком стоимость logn амортизируется.)

    Проблема с объединением этого с функциональным программированием состоит в том, что есть общее обещание, что, когда я даю вам новую структуру данных, старая не изменяется, так что в качестве точки общей теории, если она стоит много, чтобы преобразовать некоторый X, тогда существует допустимый шаблон использования, который тратит n усилий на создание X по-прежнему, но затем использует его m раз по-разному (потому что он не изменен!), в результате чего O (f (n)) стоит каждый раз: так теперь, когда вы пытаетесь амортизировать, вы получаете только O (mf (n)/n), и если m, скажем, масштабируется пропорционально n, вы уклоняетесь от этой стоимости один раз, чтобы вся структура могла в основном один раз за добавление к структуре данных. Вы не можете сказать "о, это не мой случай использования", когда вы создаете структуру данных: даже если это не ваше, это, вероятно, кто-то еще, в конце концов говоря.

    Окасаки указывает (в его тезис (PDF)), что лень (с воспоминаниями) на самом деле является именно тем, что нам нужно, чтобы закрыть этот пробел: предположим, что X имеет постпроцессовую версию, хранящуюся как ленивое значение, тогда каждый вызов преобразовать X будет иметь одно и то же ленивое значение и даст тот же memoized ответ. Итак: если вы умело переместите этот материал в thunks, то тот факт, что Haskell не перекомпоновал thunks, может использоваться для создания аргументов memoization.

  • В другом примере ++ в Haskell - операция O (1); со строгими списками, добавляющими список размером n в конец списка размера m, требуется O (m) распределение памяти вверх, так как передний список должен быть полностью перестроен; stream concatenation преобразует это в O (m) условные операции (которые, к счастью, играют очень хорошо с предсказателем ветвления в процессоре!) и распределяет эту стоимость над каждым чтением списка.

  • Лень имеет большой потенциал, если вы не используете кучу данных, но вы не знаете, какой материал вы используете или нет. Чтобы взять простой пример, если вам пришлось неоднократно инвертировать какую-то дорогостоящую монотонную функцию, которую трудно предсказать на каком-то ограниченном интервале, у вас может не быть закрытой формы для обратного или быстрого выражения для функции или ее производной (чтобы используйте Ньютон-Рафсон). Вместо этого вы могли бы построить большое двоичное дерево поиска постоянной глубины, узлы которого аннотируются с f (x), а листья - x; то вы инвертируете f для некоторого ввода x путем вычисления f (x) и двоичного поиска x. Каждый запрос затем автоматически запоминается, поэтому поиск значений рядом с другими приводит к асимптотическому ускорению из-за того же кэширования (при потенциальной стоимости постоянно растущей памяти).

Итак, когда помогает строгость?

Реальный случай, когда вы хотите удалить лень, - это рекурсивные структуры данных, и даже тогда это применимо только в том случае, если вы знаете, что хотите, чтобы вся структура данных была доступна в памяти (т.е. вы собираетесь использовать все это). Такие структуры данных обычно строгие. список, содержащий thunks для фактических значений, но указатели на другие узлы списка на 100% строгие.

Когда эти условия являются истинными, тогда не имеет смысла вкладывать лень на каждый из этих узлов, поскольку он обеспечивает дополнительную стоимость O (n) для оценки всех этих thunks и может взорвать стек вызовов до вашей рекурсии если вы не используете аннотации строгости, чтобы сохранить их. Если вы на это не на 100% поняли, лучшие объяснения, которые я видел для этого, - это такие, как это, оправдывая необходимость foldl' в случаи, когда оба foldr и foldl переполняют стек вызовов по разным причинам. Эти объяснения обычно очень практичны.

Строгость также может поставить кучу стоимости авансом, например, когда вы хотите сделать игру: если вы создаете игровой мир лениво, тогда вы можете заметить "буферизацию", когда вы войдете в совершенно новую область; но если вы можете генерировать эти вещи строго заранее, вам придется заплатить более раннюю стоимость, но вы получите более выгодную выгоду. Люди не против дожидаться, когда они нажимают кнопку "Загрузить игру", они действительно ненавидят ожидание, когда она прерывает погружение в какую-то историю. (Фактически параллельная лень действительно идеальна для этого: вы хотите, чтобы иметь возможность затормозить удар в фоновом режиме, прежде чем он вам понадобится, и в то время как действие немного светлее, чтобы результаты были доступны к тому моменту, когда вы хотите их. то, хотя, я имею в виду, что как TES3: Morrowind работал, но они включали набор свитков как кляп, который позволяет вам прыгать на полпути через игровой мир, если вы можете выжить при посадке - и скорости, которые вы что вы будете летать через регионы быстрее, чем система может их загрузить, поэтому он будет постоянно давать вам 3 секунды полета на полете, прежде чем остановиться на 2, чтобы сказать "Loading...", снова и снова, поскольку вы таким образом, пересек игровой мир. Ничто не может реально предотвратить эту проблему.)

Когда мне нужно его исправить, как я могу его исправить?

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

Как насчет создания рекурсивной структуры данных, такой как альтернативный тип списка data NonNullList x = NNL x !(Maybe (NonNullList x)), который обязательно должен иметь хотя бы один элемент? В этом случае рекурсия живет в Maybe, и как мы это исправим?

Да, вы можете использовать строгий, возможно. Однако вы можете также встроить структуру, чтобы сделать ее строгой. В этом случае вы должны написать:

data NonNullList x = End x | Continue x !(NonNullList x)

Если в вашей структуре данных слишком много повторной информации (возможно, мы храним много метаданных в нашей структуре данных) и слишком много вызовов Maybe (MyDataStructure x), тогда нам, возможно, придется иметь data MyDataStructureDescriptor = MDSD { property1 :: !String, property2 :: !Int, ...}, чтобы многие повторения этого дескриптора могут быть сведены к одному общему формату. Это действительно может быть очень хорошо для организации вашего кода.