We considered adding an nargout-like feature early on in the design of Julia and could never come up with anything that was sensible or appealing. In retrospect, I’m glad we didn’t because we’ve collectively managed quite well without, as have most programming languages. At this point I just don’t see the need: if you want to compute different subsets of things, use different functions.
In case you are unaware, this wouldn’t work in current Julia, as return-type annotations are not like regular type-annotation, they are just syntactic sugar to insert a call to convert
before the function returns. So they have nothing special compared to functions which don’t have annotations, so in a call like F = eigvals(A)
, there is really no way to know which version you would want to call.
Yes, I am aware of that. As I’ve mentioned above, this is a proposal to implement multiple dispatch on function return type, which presently does not exist in Julia.
Ok, I assumed otherwise as I had the impression that you thought this feature wouldn’t be breaking, but it seems it would be and require waiting Julia version 2.0 to be implemented, as these return-type annotations already have an incompatible meaning.
Multiple dispatch on function output types is more general than nargout
, I just used it as an example.
I don’t think it would be breaking at all. Do you have an example of what it would break?
You can’t really do multiple dispatch on output types in a dynamic language in any reasonable way. Fundamentally, it requires a well-defined notion of the type of an expression, which, by definition, a dynamic language doesn’t have. The only thing that seems sane is to say that an expression means what it means and it’s calling context does not affect that.
What is wrong with the syntax I suggested above?
I don’t even want to start thinking about how dispatch on return type would interact with current dispatch, so I will take the simplest example I can think of which doesn’t involve input arguments:
f()::Int = 1
f() = 2
local x::Int
x = f()
Currently, this assigns 2
to x
. With your suggestion, this would return 1
.
Having a syntax is not the problem. If you want to do something like that, you can pass NTuple{2, Array}
as an argument to eigvals
and dispatch on it. You’re essentially proposing a weird syntax for doing that. What’s wrong with the existing syntax? Your proposal is breaking because it suggests that
local F:: NTuple{2, Array}
F = eigvals(A)
would do something different than what it currently does, which is breaking because it’s already valid code and making it do something else is the very meaning of “breaking change”.
Yes, I see your point. In the beginning of the discussion, I proposed
optional variable declarations, that would avoid interaction with the existing code.
That said, if a user really wants this, it would be quite simple to define a
@ImissMATLAB f(args...; kwargs...)::T
macro that would transform to
f(T, args...; kwargs...)
and define methods of f(::Type{T}, ...)
accordingly, maybe using another similar macro.
(Not saying that this is a good idea, just pointing out that it is possible.)
And so I began by proposing “Optional variable declarations” which would not be breaking, because there’re aren’t any in existing code.
That argument applies to multiple dispatch on the input arguments as well.
As a piece of general advice if you’re interested in designing language features, it’s probably worth noting that language developers are usually conservative about features and are seldom willing to add new features to existing languages without a large body of evidence for the feature’s value – evidence that is usually accumulated over a long period of time in which similar requests come from many different kinds of users. A Discourse thread is rarely an effective forum to provide such evidence as a compelling design document will usually require several weeks of labor to author, but Discourse encourages a rapid back-and-forth style of communication that is not consistent with that level of attention to detail. An approach with higher probability of driving consensus might be to focus your energy on developing a prototype of your proposed feature; along the way, you are likely to learn more about the pros and cons and will be able to articulate them at the level of detail that is needed to overcome the conservativism that existing languages typically adopt.
Full agreement!
One place where a discourse discussion can be quite productive in this context is in your ability to directly ask the core devs about:
- what they’ve tried and/or thought about
- you can even get insights into where they think there will be issues
- their relative excitement or interest in such a feature
Those three things together can really help prevent you from spinning your own time on such a POC that’s unlikely to succeed. As always, you’re asking for volunteering of others’ time in gathering this feedback, so approaching this task with a solid respect for their time and prior work is always beneficial.
Not the way Julia is idiomatically used at present. A lot of Julia code dispatches on abstract types, or specifies no types at all. The key idea is that you should be able to write
function solve_quadratic(a, b, c)
D = sqrt(abs2(b) - 4 * a * c)
(- b + D) / (2 * a), (- b - D) / (2 * a)
end
without worrying about the actual type of a
, b
, and c
: as long as they support sqrt
and basic arithmetic, you should be fine.
This simplifies API design significantly. Yes, +(::Int, ::Int)
and +(::Float64,::Int)
technically “compute different things”, but you can abstract from that. You may be interested in
Not really: multiple dispatch allows writing genetic generic code where different types implement the same abstraction in different ways — the correct implementation of the same abstraction is selected. What you’re proposing is that different return types allow the same function name to mean different things based on how you call it, which is actually the opposite of how multiple dispatch should be used: one generic function, one meaning.
Violations of this principle are actually interesting for the lessons we learn from them. A prominent example is Common Lisp, which does have a mechanism for multiple values. The idea is that functions can examine if they are called for multiple values, and produce some of them on demand.
Notably, the CLTL2 says that
Normally multiple values are not used. Special forms are required both to produce multiple values and to receive them.
It is one of the clunkier parts of Common Lisp: because of the special forms for calling etc, use in higher order functions requires special constructs, so it does not mesh well with the most powerful features of the language.
Just don’t code up any killer tomatoes…