Letting users change the default types

It is common that programming languages have different types with similar or analogous functionality. For example, consider the integer types Int64, BigInt, or the float point types Float64, BigFloat.

The reason for this proriferation of types is that none of them is adequate for every situation. Int64 is favored when one needs high speed of execution, whereas BigInt is favored when one is doing calculations with very big numbers.

Given that for some users, the Big types might be more adequate than the 64 types, why not let the user decide which one of them is Julia’s default?

The kinds of calculations that I do are so small, that I actually have the luxury of not worrying so much about efficiency. In my case, the BigInt type is much more convenient for me than the Int64 type, because using the first I do not have to check for integer overflow.

But the only way I have of writting Julia code with the Big types in place of the 64 types is either to use the big"-" or syntax everywhere, or to start every line of code with some macro. A trivial arithmetical operation julia> 157 + 82 would become julia> big"157" + big"82" or julia> @MyMacro 157 + 82.

I have some arguments for letting the users change the default types:

  1. It seems that many programmer prefer big integers over 64 bits integers.

This might be inferred from the fact that some popular languages (among them Python, JavaScript, Haskell, Ruby) chose big integers as their default.

  1. There are languages that allow users to change the default type.

At least Haskell does this: Kwang's Haskell Blog - Type defaulting in Haskell

  1. Letting users change the default type fits very well with the philosophy and design of the Julia Language, as I understood it.Ju

Julia tries not to privilege it’s built-in types, and gives the user the flexibility to use his own custom type. In Julia we have numbers with measurement units(Unitful.jl), dual number(DualNumbers.jl), doublefloats(DoubleFloats.jl), and those numbers are just as good and efficient as the built-in numbers - something that is unimaginable in other languages.

how does this affect libraries? does each module has its own default type? What about calling other functions from other module?

The fact that most libraries are written size-agnostic, as you have noted, means users can do what they think is the best.

Check out:

6 Likes

Small (but @big :blush:) working example:

"""
    big!(expr)

replaces `Int` and `Float64` values in `expr` by big ones and returns the result.
"""
big!(expr) = expr
big!(expr::Int) = big(expr)
big!(expr::Float64) = big(expr)
function big!(expr::Expr)
    for i in eachindex(expr.args)
        expr.args[i] = big!(expr.args[i])
    end
    expr
end

"""
    @big(expr)
    @big expr

replaces `Int` and `Float64` values in `expr` by big ones and executes the result.
"""
macro big(expr) big!(expr) end

Input:

1.0π

Output:

3.141592653589793

Input:

@big 1.0π

Output:

3.141592653589793238462643383279502884197169399375105820974944592307816406286198

Input:

for n in 20:24
    println("factorial(", n, ") = ", factorial(n))
end

Output:

factorial(20) = 2432902008176640000
OverflowError: 21 is too large to look up in the table; consider using factorial(big(21)) instead

Input:

@big for n in 20:24
    println("factorial(", n, ") = ", factorial(n))
end

Output:

factorial(20) = 2432902008176640000
factorial(21) = 51090942171709440000
factorial(22) = 1124000727777607680000
factorial(23) = 25852016738884976640000
factorial(24) = 620448401733239439360000

10 Likes

This is a minor point, but I am not sure one should consider expressions mutable, it is not guaranteed that they don’t share structure. I would just generate a new expression.

2 Likes

Hi!

What do you mean? I do not feel that I understood what you said?

1 Like

I believe it was a comment on the solution offered by @genkuroki, where the big! fiction mutates expression objects.

2 Likes

I see

I understand your concern for preserving library code. I agree that keeping our libraries intact is crucial for the language.

However, Julia discourages the use of numeric literals when writting functions.

For example, the code below is regarded as bad coding practice, for being type unstable.

function f(x)
	if x >= 0
		return x
	else
		return 0
	end
end

We are encouraged to write the function f() the following way:

funcion f(x)
	if x >= zero(x)
		return x
	else
		return zero(x)
	end
end

That is, we are encouraged to use the multimethod functions zero(), one() instead of the literals 0, 1.

Because Julian code tends to be generic, I do not think that making the numeric literals dependant on some global state would cause much damage to our code-base.

A well-written Julia function is type stable and can accept a sufficiently wide range of arguments. Therefore, there is no need to convert the numeric values in the function code to BigInt or BigFloat.

If we just give values of BigInt or BigFloat type as arguments to the function, then it should perform the calculation with sufficient digits and sufficient precision.

Therefore, I think that the only correct use of the @big macro introduced above is to simply convert the types of the values given as function arguments to BigInt or BigFloat easily.

Example:

@big @eval begin
    n = 57
    c = Tuple(1/factorial(k) for k in 0:n)
end
evalpoly(1, c)

2.718281828459045235360287471352662497757247093699959574966967627724076630353555

@big exp(1)

2.718281828459045235360287471352662497757247093699959574966967627724076630353555

typeof(c)

NTuple{58, BigFloat}

3 Likes

Your macro does solve the problem I am facing. The only thing is that it is not very convenient to write a macro invocation in every line of my code.

But perhaps it might be possible to configure Julia’s REPL so that it automatically applies your macro to every line of code. Someone who knows Julia’s internal much better than I do might be capable of doing that.

I recommend the following way:

  1. Write type-stable functions that can be computed with high precision by simply giving values of BigInt or BigFloat type as arguments.

  2. If necessary, provide BigInt or BigFloat type arguments to these functions. Use the @big macro only in this case.

If you want to act the @big macro on multiple lines at once, enclose the lines with @big @eval begin and end.

1 Like

This is exactly what the SafeREPL package linked above does:

2 Likes

I see, they have a SwapLiterals subpackage that lets me change the meaning of Julia literals :grinning:

SafeREPL has just saved my life! I can hardly overstated how much the package makes my life easier

2 Likes