Будет ли значение, которое имеет тип с ограничениями класса, фактически является функцией во время выполнения?
Рассмотрим знаменитый
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Предположим, что, чтобы избежать ограничения мономорфизма, это аннотируется с:
fibs :: Num a => [a]
Это означает, что во время выполнения значение списка fibs
действительно не существует, а скорее функция, которая вычисляет список заново каждый раз, когда выбран элемент fibs
?
Вопрос в том, как такие случаи фактически обрабатываются в разных реализациях Haskell, о которых вы знаете.
--- ДОБАВЛЕНО ----
Я чувствую, что я должен уточнить немного больше. Рассмотрим:
fibsInteger :: [Integer]
fibsInteger = 0: 1: zipWith (+) fibsInteger (tail fibsInteger)
и предположим, что во время выполнения программы значение
(fibsInteger !! 42)
необходимо оценить. В этом случае я ожидал бы, что последующие оценки, подобные этому, обнаружат, что первые 43 элемента fibsInteger
уже оценены. Это означает также, что fibsInteger
сам и первые 42 его хвоста уже находятся в WHNF.
Но это было бы невозможно с полиморфным fibs
, насколько я вижу. Замечание FUZxxl
потому что класс типа обычно вводит новый аргумент, содержащий словарь с функциями этого класса
похоже, поддерживает мое мнение, что значение, подобное fibs
, эффективно отображается как функция во время выполнения?
Если бы это было так, то приложение, подобное ((maximum . map (fibs!!)) [100000 .. 101000] :: Integer)
shold, получило значительно больше времени для оценки, чем неполиморфный вариант ((maximum . map (fibsInteger!!)) [100000 .. 101000] :: Integer)
, потому что первые 100000 номеров нужно будет пересчитывать каждый раз.
(К сожалению, я не могу попробовать это в это время)
Ответы
Ответ 1
Это зависит от реализации. В GHC классы классов реализуются с использованием словарей. Скажем, класс Num
был определен следующим образом (упрощенный для этого примера):
class Num a where
fromInteger :: Integer -> a
(+) :: a -> a -> a
Затем он будет скомпилирован как "тип словаря":
data Num a = Num { fromInteger :: Integer -> a, plus :: a -> a -> a }
Все, что связано с ограничением Num
, получит дополнительный аргумент для словаря, поэтому, например, foo x = x + 1
станет:
foo :: Num a -> a -> a
foo num x = plus num x (fromInteger num 1)
Итак, посмотрим, как GHC компилирует fibs
, будем ли мы?
$ cat Fibs.hs
module Fibs where
fibs :: Num a => [a]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
$ ghc -c Fibs.hs -ddump-simpl
==================== Tidy Core ====================
Rec {
Fibs.fibs [Occ=LoopBreaker]
:: forall a_abu. GHC.Num.Num a_abu => [a_abu]
[GblId, Arity=1]
Fibs.fibs =
\ (@ a_akv) ($dNum_akw :: GHC.Num.Num a_akv) ->
GHC.Types.:
@ a_akv
(GHC.Num.fromInteger
@ a_akv $dNum_akw (GHC.Integer.smallInteger 0))
(GHC.Types.:
@ a_akv
(GHC.Num.fromInteger
@ a_akv $dNum_akw (GHC.Integer.smallInteger 1))
(GHC.List.zipWith
@ a_akv
@ a_akv
@ a_akv
(GHC.Num.+ @ a_akv $dNum_akw)
(Fibs.fibs @ a_akv $dNum_akw)
(GHC.List.tail @ a_akv (Fibs.fibs @ a_akv $dNum_akw))))
end Rec }
Если вы немного прищурились, это существенно
fibs :: Num a -> [a]
fibs num = fromInteger num 0
: fromInteger num 1
: zipWith (plus num) (fibs num) (tail (fibs num))
Итак, для GHC ответ да. Как вы подозревали, это может иметь серьезные последствия для производительности, поскольку это разрушает совместное использование fibs
, на которое опирается это определение, до того момента, когда вы получаете экспоненциальную рабочую среду вместо линейного 1.
Prelude Fibs> :set +s
Prelude Fibs> fibs !! 30
832040
(3.78 secs, 912789096 bytes)
Мы можем решить эту проблему, представив себе:
module SharedFibs where
fibs :: Num a => [a]
fibs = let f = 0 : 1 : zipWith (+) f (tail f) in f
Это намного лучше.
Prelude SharedFibs> :set +s
Prelude SharedFibs> fibs !! 30
832040
(0.06 secs, 18432472 bytes)
Prelude SharedFibs> fibs !! 100000
<huge number>
(2.19 secs, 688490584 bytes)
Но у него по-прежнему возникает та же проблема, что fibs
не разделяется между отдельными вызовами. Если вы хотите этого, вам нужно будет специализировать fibs
на нужный тип номера в let
или where
.
Подобные сюрпризы производительности являются частью причины, по которой существует опасное ограничение мономорфизма.
1 Игнорируя тот факт, что дополнение Integer
не является постоянным.
Ответ 2
Полиморфизм может принести дополнительную нагрузку на производительность (я думаю, это вопрос, который вы задаете). В ответ Томаса на этот вопрос, делая тип неполиморфного сокращенного времени выполнения от 36 до 11 секунд.
Ваше выражение:
Это, по-видимому, означает, что во время выполнения значение fibs списка действительно не существует, а скорее функция, которая вычисляет список заново каждый раз, когда элемент fibs выбран?
Я не совсем уверен, что вы имеете в виду здесь - вы, кажется, знаете, что он ленив. Вы можете спросить, считает ли Haskell это "объявление функции" или "объявление значения" - вы можете попробовать использовать шаблон Haskell:
> runQ [d| fib = 0 : 1 : zipWith (+) fib (tail fib) |]
[ValD (VarP fib) ...
Итак, это объявление значения (ValD).
Ответ 3
Прежде всего, список бесконечен, поэтому невозможно сгенерировать весь список до запуска программы. Как уже указывал MatrixFrog, fibs
является thunk. Вы можете грубо представить себе thunk как функцию, которая не принимает аргумента и возвращает значение. Единственное отличие состоит в том, что указатель на функцию заменяется указателем на результат после этого, заставляя результат кэшироваться. Это происходит только в случае функций, которые не зависят от какого-либо типа, потому что в классе типов обычно вводится новый аргумент, содержащий словарь с функциями этого класса (этот процесс иногда называют reification).
Долгое время я отправил ответ на этот вопрос codegolf.SE, содержащий собственную реализацию thunks в C. Код не очень хорош, список вещей не очень хорошо отделяется от самого мошенника, но стоит посмотреть.
Ответ 4
Функция всегда включает конструктор типа (->)
, поэтому он не является функцией. Это значение. Функции также являются значениями, но значения не являются функциями, и это не имеет ничего общего с лени. Ключевое свойство функции состоит в том, что вы можете применить ее. Приложение имеет тип:
(a -> b) -> a -> b
Конечно, это ленивое значение, а на уровне реализации подразумевается что-то, называемое thunk, но это в значительной степени не имеет отношения к вашему вопросу. Thunks - это деталь реализации. Просто потому, что это лениво вычисленное значение не превращает его в функцию. Не путайте оценку с исполнением! Функции на языке типа C не совпадают с функциями в Haskell. Haskell использует реальное математическое понятие функции, которое совершенно не связано с тем, какие действия стратегии выполняются на уровне машины.