Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lack of convert(::Interval{T}, a::Real) #580

Closed
lairez opened this issue Aug 31, 2023 · 3 comments · Fixed by #590
Closed

Lack of convert(::Interval{T}, a::Real) #580

lairez opened this issue Aug 31, 2023 · 3 comments · Fixed by #590

Comments

@lairez
Copy link

lairez commented Aug 31, 2023

I know I am late to the debate (see #558), but I faced the problem of missing conversions in my project. I am implementing arithmetic circuits, so there is a type T for constants, typically Interval{Float64}, but anything with + and * defined is fine. To be easily usable, we need to convert the user-provided constants (typically Int) to T. Why would this work for literally any number type on earth and fail for Interval{Float64}?

I understand that promotion is tricky, but conversion is not! The interval method, in the form interval(T, a), is a conversion function. So the conversion function exists! It seems very wrong for a Julia library to refuse to fit the general conversion system. Is think that convert(::Type{Interval{T}}, a::Real) = interval(T, a) or Interval{T}(a::Real) = interval(T, a) are not wrong in any way.

The pro of the PR #558 is “enforce the splitting between Interval and Number by preventing all conversion except between Intervals”. In the future, will you also refuse to compile 2*interval(0.0,1.0) because 2 is not an interval? After all, this is a kind of implicit conversion. #558 mentions a debate on the matter in #555, but I don't see it.

Concrete example

I understand that you are not fond of promoting the elements of [1, 2, 3, 4, interval(5., 6.)] to anything. So I am fine with the current behavior

julia> [1, 2, 3, 4, interval(5., 6.)]
5-element Vector{Real}:
   1
   2
   3
   4
 [5.0, 6.0]

But what about Interval{Float64}[1, 2, 3, 4, interval(5., 6.)]?

julia> Interval{Float64}[1, 2, 3, 4, interval(5., 6.)]
ERROR: MethodError: no method matching Interval{Float64}(::Int64)

The user is explicitly telling you he wants a conversion. Why would you force him into non-generic code? Why refusing the convert mechanism? It is simple and non surprising (as opposed to the promotion system).

Current solutions (for me)

  • Avoid conversions. Concretely, I write interval(-40)*x^4-interval(10)*x^2*y-interval(7)*y^3-interval(17)*x^2-interval(75)*x*y+interval(71)*x-interval(44) instead of -40*x^4-10*x^2*y-7*y^3-17*x^2-75*x*y+71*x-44. This is unsustainable. For example, I cannot copy-paste a polynomial that I computed in some computer algebra system.
  • Type piracy. I just define convert(::Type{Interval{T}}, a::Real) = interval(T, a) and life is good for me, but we know that type piracy is not good in the long term with problems of software composability.
  • In the generic code, I replace convert(T, a) with one(T)*a. It works, but it is so ugly...
  • Write specific codes for interval types. No very Juliaesque.
@OlivierHnt
Copy link
Member

Thank you for the feedback! 🙂

Automatic conversion/promotion is dangerous since it leads to intractable silent errors. Typically, conversion issues arise when someone is relying on a code (often thought out for floating-points arithmetic) and use Interval as a drop-in replacement.
So we figured that asking people to explicitly type interval(T, x) is better at preventing mistakes than relying on the implicit Julia conversion/promotion system.

Also, you are right that we perform an implicit conversion by allowing mixing Number and Interval for +,-,*,/. This is where it was decided to draw the line; I believe everyone is on board on keeping it that way, but see also #568.

So, Interval{Float64}[1, 2, 3, 4, interval(5., 6.)] does not work for the reason mentioned above, that is one is forced to do interval.(Float64, [1, 2, 3, 4, interval(5., 6.)]).

Avoid conversions. Concretely, I write interval(-40)x^4-interval(10)x^2y-interval(7)y^3-interval(17)x^2-interval(75)xy+interval(71)x-interval(44) instead of -40x^4-10x^2y-7y^3-17x^2-75xy+71x-44. This is unsustainable. For example, I cannot copy-paste a polynomial that I computed in some computer algebra system.

May I ask why you need to convert the Int to interval? Since *,-,+,/ are still defined.

Write specific codes for interval types. No very Juliaesque.

Yes but note that interval arithmetic has specificities that often plays poorly with generic code, @Kolaru gave a nice example recently:

function f(x, y)
    if x == y
        return 1
    else
        return exp(x - y)
    end
end

@lairez
Copy link
Author

lairez commented Aug 31, 2023

May I ask why you need to convert the Int to interval? Since *,-,+,/ are still defined.

In my case, this is similar to symbolic computation, when I write 2*x, I mean the circuit which multiplies 2 with the circuit x (of type, for example, Circuit{Interval{Float64}}, which is a circuit whose constants are in Interval{Float64}. So no arithmetic operation is actually performed, so we cannot rely on * to convert 2 implicitly.

Yes but note that interval arithmetic has specificities that often plays poorly with generic code

I am well aware of that, and this is mostly related to boolean tests. In IntervalArithmetic.jl, many of them are removed, including ==, so this is enough to forbid Kolaru's example.

So we figured that asking people to explicitly type interval(T, x) is better at preventing mistakes than relying on the implicit Julia conversion/promotion system.

Forbidding conversions prevents mistakes by forbidding generic programming! This is too high of a cost. This is like forbidding cars to prevent car accidents. (Promotion is tricky, I really think it should be separated from conversion.)

Practical case: a library for arithmetic circuits

On the one hand, we have Circuit.jl which provides code to deal with arithmetic circuit over any type with + and * and relies on convert(T, ...) to store the user-input constants where they need to be.

On the other hand, we have IntervalArithmetic.jl providing a type Interval{Float64} but forbids conversions.

In the middle, we have some user wanting to use the latter with the former.

  • The user is really annoyed to have to write a contorted sed script between Maple and Julia to wrap every integer into an interval call.
  • The author of Circuit does not want to add specific code, even though this is just one line, because he does want IntervalArithmetic as a dependency.
  • The authors of IntervalArithmetic does not want to comply with the standard conversion mechanism.

What can the user do?

Is #568 a solution?

I understand that an interval computation is only as safe and meaningful as the input is. So this is a reason to force the user to explicitly write interval in the input, this is a kind of “Term of services”. But then we should also forbid *(::Real, ::Interval), as suggested in #568. So let's have a sheltered mode where all of this is forbidden, and a compatibility mode where all of this, including conversions is allowed and under the responsibility of the user.

In the end, it should be acknowledged that no type system will ensure mathematical soundness. A bit of type constraints helps a lot with stupid but common mistakes, but a some point there are diminishing returns and the cost in terms of usability and interoperability is always higher.

@OlivierHnt
Copy link
Member

OlivierHnt commented Aug 31, 2023

When we discuss these aspects, I found that we tend to circle back to "why not just use decorated intervals". Whenever Number and Interval are being mixed (e.g. with * or convert), this would just be recorded in an appropriate decoration so that the computation still runs but its output is flagged. The user is then warned that their result may not be rigorous.
This would solve #568 with a slightly different approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants