Hello,
I know that Julia has a function called factorial
. I have written a program as follows to do this:
function fact(a)
F = 1
for n = 1 : (a - 1)
F = F * (n + 1)
println(F)
end
end
Is it OK?
Thank you.
Hello,
I know that Julia has a function called factorial
. I have written a program as follows to do this:
function fact(a)
F = 1
for n = 1 : (a - 1)
F = F * (n + 1)
println(F)
end
end
Is it OK?
Thank you.
In what sense? What do you want to achieve? What should be critiqued?
A couple of things I notice:
Int64
. You could consider to use the type of the argument to perform the computationFrom @hack3rcon’s previous post (Questions about a starting language, is Julia good (enough)), they are just learning how to program for the first time and want pedagogical feedback.
For beginners
function myfactorial(n)
result = big(1)
for i = 1:n
result *= i
end
return result
end
println("factorial(13) = $(myfactorial(13))")
Advantages of BigInt
Always return the correct result
Disadvantages of BigInt
very SLOW
Why not use Int64 instead
Because at certain input values, the Int64 result will be incorrect due to overflow error. Beginners need to learn Integer overflow error due to the physical structure of how a computer stores the value of Int64
Will BigInt always return the correct answer?
No, at certain point, even BigInt will fail.
julia> myfactorial( parse(BigInt,"9999999999999999999999999999999999999999999999999999999999999999999999999999999") )
It’s a clear for loop in a concise method, keep doing that in general. What’s worth working on is the starting value; others have mentioned it, but I have more thoughts on it. F=1
starts with a Int64
. The type of the elements n = 1 : (a-1)
will depend on a
, so the type of F = F * (n + 1)
also depends on a
(for example, try fact(5.0)
). When a variable changes its runtime type, it’s called type instability. It’s not wrong, exactly, because Julia allows it by design, but there are performance and correctness benefits to type stability so thinking about how types might change is a good habit. If you want to work with generic types and accept the overflow issues, you could replace 1
with one(a)
. Otherwise I’d unconditionally start with BigInt
like StevenSiew suggests, and I’d additionally limit the argument myfactorial(n::Integer)
so we don’t unnecessarily involve other numerical types like BigFloat
.
Now I’d like to talk about what base Julia does, just for context on how factorial functions could be implemented. Julia has a factorial(n::Integer)
method that uses a for-loop starting at a generic type, but that method is not actually used by base integer types. Most of the other methods taking the base integer types check lookup tables with all the precomputed values that don’t overflow, and otherwise throws an error informing you of the need for a higher capacity type. If you don’t want to expend the memory for lookup tables, you can at least precompute the minimum a
to overflow a particular type and throw an error when users try to exceed it. The factorial(n::BigInt)
and factorial(n::BigFloat)
methods wrap the corresponding functions in the GMP and MPFR libraries, which I don’t know anything about.
Note that you are computing a product of a sequence starting with one, possibly with a generic type. That is exactly what the prod(itr; [init])
method does, only it exposes the starting point and the sequence (and thus their types) to the call. So, an equivalent to StevenSiew’s myfactorial(5)
would be prod(1:5; init = big(1))
. The starting point is optional and defaults to the sequence’s start, so prod(1:5; init=1) == prod(1:5)
. The advantage of prod
is that a fraction of factorials is often used to express a product of an integer sequence, e.g. the permutation formula n!/(n-r)! is expressing prod(n-r+1:n)
, and it saves far more time and is far less prone to overflow than factorial(n)÷factorial(n-r)
.