Во-первых, убедитесь, что вы компилируете с -O2
. Это делает многие проблемы с производительностью просто исчезают:)
Первая проблема, которую я вижу, это то, что null
- это просто имя переменной. Вы хотите []
. Здесь это эквивалентно, потому что единственными вариантами являются x:xs
и []
, но это не всегда будет.
Проблема здесь проста: когда вы звоните sum [1,2,3,4]
, это выглядит так:
1 + (2 + (3 + (4 + 0)))
, не сводя ни одного из этих дополнений к числу , из-за нестрогой семантики Haskell. Решение простое:
myAdd = myAdd' 0
where myAdd' !total [] = total
myAdd' !total (x:xs) = myAdd' (total + x) xs
(Вам понадобится {-# LANGUAGE BangPatterns #-}
в верхней части исходного файла, чтобы скомпилировать это.)
Это накапливает сложение в другом параметре и на самом деле является хвостовой рекурсией (ваш не; +
находится в хвостовой позиции, а не myAdd
). Но на самом деле это не совсем хвостовая рекурсия, о которой мы заботимся в Хаскеле; это различие в основном относится к строгим языкам. Секрет в этом * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 10 * * * * * * * * *, поэтому он вычисляется каждый раз, когда вызывается * 10 27 *, поэтому никакие неоцененные дополнения не создаются, и он работает в постоянном пространстве. В этом случае GHC может на самом деле понять это с помощью -O2
благодаря своему анализу строгости, но я думаю, что обычно лучше четко указывать, что вы хотите строгого, а что нет.
Обратите внимание, что если добавление было ленивым, ваше определение myAdd
будет работать нормально; проблема в том, что вы выполняете ленивый обход списка с помощью строгой операции, которая в итоге вызывает переполнение стека. Это в основном приходит с арифметикой, которая строга для стандартных числовых типов (Int, Integer, Float, Double и т. Д.).
Это довольно уродливо, и было бы больно писать что-то подобное каждый раз, когда мы хотим написать строгий сгиб. К счастью, у Хаскелла есть абстракция, готовая к этому!
myAdd = foldl' (+) 0
(Вам нужно будет добавить import Data.List
для компиляции.)
foldl' (+) 0 [a, b, c, d]
аналогично (((0 + a) + b) + c) + d
, за исключением того, что при каждом применении (+)
(как мы называем двоичный оператор +
как значение функции), значение принудительно оценивается. Получающийся в результате код становится чище, быстрее и проще для чтения (как только вы узнаете, как работает свертывание списка, вы сможете понять любое определение, написанное на их основе, проще, чем рекурсивное определение).
По сути, проблема здесь не в том, что компилятор не может понять, как сделать вашу программу эффективной, а в том, что, сделав ее настолько эффективной, насколько вам нравится, вы можете изменить свою семантику , что оптимизация никогда не должна делать. Нестрогая семантика Haskell, безусловно, представляет собой кривую обучения для программистов на более «традиционных» языках, таких как C, но со временем это становится проще, и как только вы увидите мощь и абстракцию, которые предлагает нестрогость Haskell, вы никогда не захотите идти назад:)