Factorial program

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:

  • typically variable names start with a lowercase letter and only typenames are capitalized
  • your function does not return the value but only prints it to stdout
  • his function always returns an Int64. You could consider to use the type of the argument to perform the computation

From @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.

3 Likes

For beginners

  1. Use BigInt
  2. use return
  3. print out the result outside of the function
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") )
3 Likes

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).

2 Likes