The first runs in linear[1] time and the last in constant time. One can write it more readably:
sum_arithmetic_progression first last length =
-- to compute S = first + (first+k) + (first+2k) + … + (first+length*k = last)
-- write it backwards and add corresponding terms:
-- 2S = (first+last) + ((first+k)+(last-k)) + … + (last+first)
-- = (first+last) + … + (first+last)
-- \____ length times ___________/
(first + last) * length `div` 2
-- = sum [i | i<- [1..upperBound], i%n == 0] but fast
sum_multiples n upperBound =
sum_arithmetic_progression n last count where
(count,r) = upperBound `divMod` n
last = upperBound - r
subsets [] = [[]]
subsets (x:xs) = [set | sub <- subsets xs, set <- [x:sub,sub]]
-- given some sets [A,B,…,C] and a function count_intersection which acts like the sum of some function on the elements in the intersection of a list of sets, compute the count of their union using the inclusion–exclusion principle.
inclusion_exclusion count_intersection sets =
sum [count_intersection sub * (if odd (length sub) then 1 else -1) | sub <- subsets sets, sub != []]
answer = inclusion_exclusion sum_multiples' [3,5] where
sum_multiples' factors =
-- the sum of the numbers which are multiples of all the numbers in factors is the sum of the multiples of their lowest common multiple
sum_multiples (foldr lcm 1 factors) 1000
The point of project Euler is not to make you write clean or clear code but rather to make you write code that exploits the mathematical structure of the problem to find the answer with less computation.