[Haskell] Ленивость и строгость
От: jpx http://sourceforge.net/projects/sqlg2
Дата: 09.03.09 17:25
Оценка:
Не флейма ради, а для прояснения некоторых моментов — по мотивам недавнего обсуждения. Кое-что уже было здесь: http://www.rsdn.ru/forum/message/3045739.1.aspx
Автор: Vintik_69
Дата: 03.08.08
, но хочется еще.

Тут приводился исвестный пример вычисления среднего значения в списке, захотелось его допилить:
-- Попытка 1:
mysum :: [Integer] -> Integer
mysum xs = sum' xs 0
 where 
  sum' [] acc = acc
  sum' (x:xs) acc = sum' xs (acc + x)

mean1 :: [Integer] -> Double
mean1 xs = fromIntegral (mysum xs) / fromIntegral (length xs)

-- Попытка 2:
mean2 :: [Integer] -> Double
mean2 xs = fromIntegral s / fromIntegral l
 where
  (s, l) = mean' xs 0 0
  mean' :: [Integer] -> Integer -> Integer -> (Integer, Integer)
  mean' [] accSum accLen = (accSum, accLen) -- ((,) $! accSum) $! accLen
  mean' (x:xs) accSum accLen = mean' xs (accSum + x) (accLen + 1)

-- Пробуем:
n = 10000000
list = [1..n]

--main = putStrLn $ show $ mysum list
--main = putStrLn $ show $ mean1 list
main = putStrLn $ show $ mean2 list


Замечания и вопросы:

  1. Как компилятор (с опцией -O) понимает, что функция mysum — строгая? Я несколько раз встречал упоминания о том, что '+' — строгий, но непонятно, откуда это — в Haskell Report я ничего не нашел. Можно ли всегда на это полагаться или это фича GHC?

  2. Почему mean1 жрет память в объеме O(N), понятно — см. http://cgi.cse.unsw.edu.au/~dons/blog/2008/05/16, хотя если при таком тривиальном изменении программы приходится смотреть на Core Haskell, это неуютно как-то.

  3. Можно ли как-то заставить работать mean2 без использования лишней памяти, не прибегая к хакам вроде $! или seq?

  4. Правильно ли я понимаю, что stack space overflow выдается из-за того, что начинает вычисляться отложенное вычисление (...((0+1)+2)+...+N), а не из-за того, что компилятор не сделал tail-call optimization?
Re: [Haskell] Ленивость и строгость
От: jpx http://sourceforge.net/projects/sqlg2
Дата: 09.03.09 17:45
Оценка:
По поводу 3-го вопроса я что-то стормозил, делается просто:
mean2 :: [Integer] -> Double
mean2 xs = mean' xs 0 0
 where
  mean' :: [Integer] -> Integer -> Integer -> Double
  mean' [] accSum accLen = fromIntegral accSum / fromIntegral accLen
  mean' (x:xs) accSum accLen = mean' xs (accSum + x) (accLen + 1)


Но опять же, почему деление строгое?
Re[2]: [Haskell] Ленивость и строгость
От: BulatZiganshin  
Дата: 09.03.09 18:31
Оценка:
Здравствуйте, jpx, Вы писали:

jpx>Но опять же, почему деление строгое?


значит, как работает анализатор строгости:

есть у тебя к примеру функция

f x y | y>0 = ...
f x y = ...

анализатор смотрит на это дело и видит, что без знания значения y мы не сможем дальше продвинуться в вычислении f. в данном случае он нам банально необходим для выбора альтернативы. это называется фукнция f строга по аргументу y. строго говоря функция строга по аргументу, если f _|_ = _|_, но это то же самое, только выраженное более математическим языком — значение функции вычислить не удастся если значение аргумента неизвестно

функции +,/ строги, потому что ghc проанализировал их определения и нашёл что они удовлетворяют этому условию
Люди, я люблю вас! Будьте бдительны!!!
Re[2]: [Haskell] Ленивость и строгость
От: deniok Россия  
Дата: 09.03.09 18:34
Оценка:
Здравствуйте, jpx, Вы писали:

jpx>Но опять же, почему деление строгое?


А в каком смысле встроенная функция может быть нестрогой? Если у нас есть ленивая myFunc a b, то в рантайме STG-машины происходит обращение к коду myFunc и выполняются редукции, которые там есть. Дойдет ли дело до аргументов — выясняется в процессе этих редукций. А для (+) a b никаких редукций в плюсе (или, там, делении) нету. Это же не числа Пеано, Int'ы складываются атомарно, с помощью средств целевой платформы. Если рантайм добрался до (+), то для вызова низкоуровневого кода сложения аргументы необходимы.
Re: [Haskell] Ленивость и строгость
От: BulatZiganshin  
Дата: 09.03.09 18:40
Оценка:
Здравствуйте, jpx, Вы писали:

jpx>
  • Почему mean1 жрет память в объеме O(N), понятно — см. http://cgi.cse.unsw.edu.au/~dons/blog/2008/05/16, хотя если при таком тривиальном изменении программы приходится смотреть на Core Haskell, это неуютно как-то.

    не надо воспринимать возможность работать с большими списками в O(1) памяти как всегда существующую. считай это полезным хаком, которого можно добиться, посмотрев в core или просто понимая как работает laziness. н хочешь думать — получаешь с гарантией поведение не хуже чем в обычных языках

    jpx>
  • Можно ли как-то заставить работать mean2 без использования лишней памяти, не прибегая к хакам вроде $! или seq?

    есть ли хак, который позволяет обойтись без хаков? сейчас достаточно поставить ! перед именем аргумента, чтобы он стал строго вычисляться при вызове функции

    jpx>
  • Правильно ли я понимаю, что stack space overflow выдается из-за того, что начинает вычисляться отложенное вычисление (...((0+1)+2)+...+N), а не из-за того, что компилятор не сделал tail-call optimization?

    не понятно, где именно, но всё равно да. mysum у тебя создаёт такой thunk и вероятно mean2 (без кахов ) тоже

    вообще, добиться от ghc чтоб он не делал tail-call optimization — это надо постараться
  • Люди, я люблю вас! Будьте бдительны!!!
    Re[3]: [Haskell] Ленивость и строгость
    От: BulatZiganshin  
    Дата: 09.03.09 18:44
    Оценка: +1
    Здравствуйте, deniok, Вы писали:

    D>А в каком смысле встроенная функция может быть нестрогой?


    а это не встроенные функции. встроенные +-*/ работают с небоксированными значениями, и они конечно не могут быть ленивыми а определения +-*/ дебоксируют аргументы и вызывают встроенные функции, типа такого:

    instance Num Double
    (D# x) + (D# y) = D# (x +## y)
    Люди, я люблю вас! Будьте бдительны!!!
    Re[4]: [Haskell] Ленивость и строгость
    От: deniok Россия  
    Дата: 09.03.09 19:22
    Оценка:
    Здравствуйте, BulatZiganshin, Вы писали:

    BZ>Здравствуйте, deniok, Вы писали:


    D>>А в каком смысле встроенная функция может быть нестрогой?


    BZ>а это не встроенные функции. встроенные +-*/ работают с небоксированными значениями, и они конечно не могут быть ленивыми а определения +-*/ дебоксируют аргументы и вызывают встроенные функции, типа такого:


    BZ>instance Num Double

    BZ> (D# x) + (D# y) = D# (x +## y)

    Да, правильно, просто топикстартеру "неуютно как-то смотреть в Core Haskell", так что я решил опустить часть уровней абстракции Боксирование, всё-таки, деталь реализации GHC. А так, да, заключение о строгости дает ее анализатор.

    Кстати, прикольно, что, если бы мы имели дело с числами Черча
    data Nat = Z | S Nat
    
    (+) :: Nat -> Nat -> Nat
    Z     + m = m
    (S n) + m = n + S m

    то второе слагаемое вполне себе может быть undefined.
    Re[5]: [Haskell] Ленивость и строгость
    От: BulatZiganshin  
    Дата: 09.03.09 19:26
    Оценка:
    Здравствуйте, deniok, Вы писали:

    D>>>А в каком смысле встроенная функция может быть нестрогой?


    D>Да, правильно, просто топикстартеру "неуютно как-то смотреть в Core Haskell", так что я решил опустить часть уровней абстракции


    насколько я понимаю, встроенная функция может быть нестрогой, к примеру par и par#
    Люди, я люблю вас! Будьте бдительны!!!
    Re[6]: [Haskell] Ленивость и строгость
    От: deniok Россия  
    Дата: 09.03.09 19:49
    Оценка:
    Здравствуйте, BulatZiganshin, Вы писали:


    BZ>насколько я понимаю, встроенная функция может быть нестрогой, к примеру par и par#


    В этом случае, как я понимаю глядя в исходники, приходится дурить компилятор:
    -- The reason for the strange "lazy" call is that 
    -- it fools the compiler into thinking that pseq and par are non-strict in 
    -- their second argument (even if it inlines pseq at the call site). 
    -- If it thinks pseq is strict in "y", then it often evaluates 
    -- "y" before "x", which is totally wrong. 
    
    {-# INLINE pseq #-} 
    pseq :: a -> b -> b 
    pseq x y = x `seq` lazy y 
    
    {-# INLINE par #-} 
    par :: a -> b -> b 
    par x y = case (par# x) of { _ -> lazy y }
    Re[3]: [Haskell] Ленивость и строгость
    От: jpx http://sourceforge.net/projects/sqlg2
    Дата: 09.03.09 20:00
    Оценка:
    Здравствуйте, deniok, Вы писали:

    D> А в каком смысле встроенная функция может быть нестрогой?


    Ну например реализация Integer может быть какой-нибудь ленивой структурой данных.

    В любом случае, это детали реализации. Такое поведение прописано в спецификации или нет, интересно?
    Re[4]: [Haskell] Ленивость и строгость
    От: deniok Россия  
    Дата: 09.03.09 20:59
    Оценка:
    Здравствуйте, jpx, Вы писали:

    jpx>Здравствуйте, deniok, Вы писали:


    D>> А в каком смысле встроенная функция может быть нестрогой?


    jpx>Ну например реализация Integer может быть какой-нибудь ленивой структурой данных.


    Можно, конечно, только в этом нет никакого прикладного смысла; у конечного пользователя нет средств использовать подобную ленивость, поскольку определение Integer не рекурсивно. Конструкторы данных у Integer примитивные 1, 42, 1000000000000001, поэтому сопоставление с образцом с частичным разбором по структуре не пройдет. А числа Черча, к несчастью, вычислительно крайне неэффективны.

    jpx>В любом случае, это детали реализации. Такое поведение прописано в спецификации или нет, интересно?


    В спецификации написано

    Function application in Haskell is non-strict; that is, a function argument is evaluated only when required.
    (6.2 Strict Evaluation)

    Но если редукции дошли до (+) x y, то вычисление x и y уже в любом случае required, не так ли?
    Re[5]: [Haskell] Ленивость и строгость
    От: jpx http://sourceforge.net/projects/sqlg2
    Дата: 09.03.09 22:33
    Оценка:
    Здравствуйте, deniok, Вы писали:

    D> А числа Черча, к несчастью, вычислительно крайне неэффективны.


    Гм

    D> Но если редукции дошли до (+) x y, то вычисление x и y уже в любом случае required, не так ли?


    Ладно, уговорили
     
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.