Practical numbers: Difference between revisions

m
(→‎Composition of pure functions: Moved comment to talk page.)
Line 181:
 
Note that no type hints are added for the compiler in this example. The additional work of adding such hints can pay off with larger projects, particularly those involving several collaborators, but the cost/benefit (work for the coder, run-time startup cost) is known to be rather less clear at a small scale.
 
Clarity about the return type semantics, is however, very useful when reasoning about pure functions, and my personal approach is to add light informal '''comments''' about the type, in a Hindley Milner idiom, which lends itself well to brief and clean notes on the type of curried functions, which are more easily composable, especially with higher order functions, and which I generally prefer to use.
 
I've been asked (always by the same person :-) why I don't find the idiom of Python compiler type-hints a good match for my semantic type comments, and the answer is essentially that the compiler type hints are not a clear or helpful notation for this purpose – not just because they generally involve more typing and visual noise, but also, and in particular, because with curried functions the compiler hint notation becomes swamped by use of the cognitively redundant `Callable` keyword, which degrades clarity, and imposes burden, for the human reader.
 
<lang python>'''Practical numbers'''
 
from itertools import accumulate, chain, groupby, product
from math import floor, sqrt
from operator import mul
from functools import reduce
 
 
# isPractical :: Int -> Bool
def isPractical(n):
'''True if n is a Practical number
(a member of OEIS A005153)
'''
ds = properDivisors(n)
return all(map(
sumOfAnySubset(ds),
range(1, n)
))
 
 
# sumOfAnySubset :: [Int] -> Int -> Bool
def sumOfAnySubset(xs):
'''True if any subset of xs sums to n.
'''
def go(n):
if n in xs:
return True
else:
total = sum(xs)
if n == total:
return True
elif n < total:
h, *t = reversed(xs)
d = n - h
return d in t or (
d > 0 and sumOfAnySubset(t)(d)
) or sumOfAnySubset(t)(n)
else:
return False
return go
 
 
# ------------------------- TEST -------------------------
# main :: IO ()
def main():
'''Practical numbers in the range [1..333],
and the OEIS A005153 membership of 666.
'''
 
xs = [x for x in range(1, 334) if isPractical(x)]
print(
f'{len(xs)} OEIS A005153 numbers in [1..333]\n\n' + (
spacedTable(
chunksOf(10)([
str(x) for x in xs
])
)
)
)
print("\n")
for n in [666]:
print(
f'{n} is practical :: {isPractical(n)}'
)
 
 
# ----------------------- GENERIC ------------------------
 
# chunksOf :: Int -> [a] -> [[a]]
def chunksOf(n):
'''A series of lists of length n, subdividing the
contents of xs. Where the length of xs is not evenly
divible, the final list will be shorter than n.
'''
def go(xs):
return [
xs[i:n + i] for i in range(0, len(xs), n)
] if 0 < n else None
return go
 
 
# primeFactors :: Int -> [Int]
def primeFactors(n):
'''A list of the prime factors of n.
'''
def f(qr):
r = qr[1]
return step(r), 1 + r
 
def step(x):
return 1 + (x << 2) - ((x >> 1) << 1)
 
def go(x):
root = floor(sqrt(x))
 
def p(qr):
q = qr[0]
return root < q or 0 == (x % q)
 
q = until(p)(f)(
(2 if 0 == x % 2 else 3, 1)
)[0]
return [x] if q > root else [q] + go(x // q)
 
return go(n)
 
 
# properDivisors :: Int -> [Int]
def properDivisors(n):
'''The ordered divisors of n, excluding n itself.
'''
def go(a, x):
return [a * b for a, b in product(
a,
accumulate(chain([1], x), mul)
)]
return sorted(
reduce(go, [
list(g) for _, g in groupby(primeFactors(n))
], [1])
)[:-1] if 1 < n else []
 
 
# listTranspose :: [[a]] -> [[a]]
def listTranspose(xss):
'''Transposed matrix'''
def go(xss):
if xss:
h, *t = xss
return (
[[h[0]] + [xs[0] for xs in t if xs]] + (
go([h[1:]] + [xs[1:] for xs in t])
)
) if h and isinstance(h, list) else go(t)
else:
return []
return go(xss)
 
 
# until :: (a -> Bool) -> (a -> a) -> a -> a
def until(p):
'''The result of repeatedly applying f until p holds.
The initial seed value is x.
'''
def go(f):
def g(x):
v = x
while not p(v):
v = f(v)
return v
return g
return go
 
 
# ---------------------- FORMATTING ----------------------
 
# spacedTable :: [[String]] -> String
def spacedTable(rows):
'''Tabulation with right-aligned cells'''
columnWidths = [
len(str(row[-1])) for row in listTranspose(rows)
]
 
def aligned(s, w):
return s.rjust(w, ' ')
 
return '\n'.join(
' '.join(
map(aligned, row, columnWidths)
) for row in rows
)
 
 
# MAIN ---
if __name__ == '__main__':
main()</lang>
{{Out}}
<pre>77 OEIS A005153 numbers in [1..333]
 
1 2 4 6 8 12 16 18 20 24
28 30 32 36 40 42 48 54 56 60
64 66 72 78 80 84 88 90 96 100
104 108 112 120 126 128 132 140 144 150
156 160 162 168 176 180 192 196 198 200
204 208 210 216 220 224 228 234 240 252
256 260 264 270 272 276 280 288 294 300
304 306 308 312 320 324 330
 
 
666 is practical :: True</pre>
 
 
===Semantic type comments replaced by compiler type hints===
Note the the prominence of the '''Callable''' keyword, and the loss of specificity for the reader, in the uses of the '''Any''' type symbol.
 
Semantic clarity for the human reader could be improved by restoring the informal – but cleaner, and more informative – type comments which this version deletes from the original version above.
<lang python>'''Practical numbers'''
 
9,655

edits