Strict typing and restricting types in a method's signature

@akis You provided incorrect information regarding when type specialization happens, in a thread where the original poster asked a simpler question. Then you replied to objections with an unclear statement about the compiler “trying” to specialize functions. We appreciate that you try to help by replying to user questions, but please keep your replies focused on these questions, and avoid asserting things when you’re unsure. This thread could have been closed with a very direct reply which would have been useful to people finding it later, but now it’s full of unrelated debates.

4 Likes

And I appreciate your effort to keep this platform clean. I fully agree that the thread got off-course and that’s a negative outcome, but that was never my choice and I wouldn’t answer further replies.

Uncertainty is a good filter to avoid some statements. However, I’m not talking on behalf of anyone but myself. And to the best of our knowledge and motivations, there will always be points of disagreement, different perspectives, misunderstandings (what may be clear to one user isn’t necessarily clear to another) and even flat-out wrong impressions. Beyond trying to avoid them, we should also learn to properly handle them when they will inevitably occur, or even take advantage of them to help users.

Problematic statements (by any criteria) are not the end of the world. Cause (among other reasons) posts are editable, and this platform also provides messaging. From the average reader’s perspective (which is my criterion when replying to user questions), @mbauman’s reply is a good example of helping by clarifying things, while @ChrisRackauckas’s replies (in this thread) are good examples of how not to do it. Cause even after reading his replies, I’m still not sure what would be a proper wording for my statements to pass the test of correctness and accuracy.

As a moderator, you can always move the resulting mess into a separate thread or even in the trash. The ideal would be for someone to isolate the correct information in this thread and combine it into a single post or even create a relevant FAQ entry and link there.

@akis, this comment is inappropriate and may violate the Julia community standards. In the future, I strongly encourage you to neither personally attack anyone nor assert that you speak on behalf of the average reader.

1 Like

Ideally people would start new threads to discuss things which are not on topic. It’s very hard to separate useful content from noise after the fact, as everything is mixed now.

That’s the opposite of what I said:

And that should be self-evident, but I have to clarify even that. I’m not the average user and nobody is. We all try to guess how other people may read our words. Correct statements expressed incomprehensibly aren’t much better than wrong statements. Moreover, even the simplest of statements can get misread by biased readers.

I know Julia community standards (I have translated them), but I’m the one personally attacked here (just count the number of quotes negatively addressing me). I wish none of you to ever come to my position. The thing is that personal attacks naturally lead to personal defense. Should I give up that right?

That’s my point. If a mix is hard for a moderator, it’s even harder for the simple reader. But moderators have ways around that. For example, after concluding that a thread has gone wrong, you can send every involved user a message with a request to remove/rephrase personal mentions and parts not directly related to the topic and a deadline to do so. I’m fine with having parts or wholes of my posts deleted, if the same happens with the attacks on me. And I’m fine beginning that process myself, if a moderator has decided that’s the best course of action. Things shouldn’t be too difficult if we focus on solutions rather than on problems.

I disagree. It’s not just for throwing errors, but like types in general it serve as useful, compiler checked documentation.

I don’t want to reopen the age old static vs dynamic typing debate. I’m a proponent of the former, and while Julia is dynamically typed, it allows me to write code in a mostly statically typed style. Many Julia programmers, like Lispers, seem to think of types only as useful for optimization, but I’d put types in many places even if it made the code a bit slower. IME your unintended uses for loose typing end up being unintended consequences.

I prefer to strictly type function arguments and return types. Not a categorical rule, but a general preference. I may leave local variables untyped if their type appears obvious. YMMV, and it obviously does.

2 Likes

Can you give an example of ‘unintended consequences’?

For me such type annotations only distract from the code. I wouldn’t/couldn’t e.g. type-annotate in R or - if I remember correctly, long time ago - in Matlab. If I need the type for dispatch it’s a different matter. In packages I’m not sure yet, but in end-user code I don’t see the point of (unnecessary) types.

Consider this, from Base. Let’s say using the function in you can’t remember if the collection or the item goes first:

a = ["a", "b", "c"]
b = "b"
in(a,b) # false
in(b,a) # true

No errors, just an unintended result.

3 Likes

True, documentation is a case that I overlooked. (In fact, it’s probably the only use for putting a return type on a type-stable function since if it’s type-stable, it would always be inferred anyways.)

But restricting types on functions can have some downsides that duck-typing fixes. Here’s a few examples:

  1. Not all “functions” are subtypes of Function. In fact, not all functions are even subtypes of Base.Callable. There’s an example that I think even shows up in the manual: polynomials a la Polynomials.jl are overloaded types which do not subtype Base.Callable. So if you’re restricting use of f(g::Base.Callable), you could be leaving out a lot of really good use cases for your functions.
  2. For iterative solvers of linear systems, you don’t necessarily want to solve Ax=b for A<:Matrix{T}, you want to solve this for any operator A which has an action Ax. By not restricting this to matrices (and slightly generalizing the interface), it allows the library automatically allow memory-efficient “matrix-free” methods.
  3. My personal example is that not all things which define +, -, /, *, and optionally sqrt are numbers (one example being an ApproxFun Fun). So by restricting inputs to numbers, I had accidenally made the numerical differential equations methods exclude natively solving such types when some may have actually worked (Fun isn’t the best example because other changes still need to happen, but you get the point).

These examples are for library building. Indeed, when building libraries what’s more important on user input are the interfaces which are implemented on the user types and the “traits” that these types have (i.e. "this code works for types which implement T*T). That allows the maximal amount of generality and, because functions auto-specialize, doesn’t lose performance. But yes, it can lead to some quirky documentation issues that I think traits will help solve in the near future.

Julia stands at an odd intersection between dynamic and static typing, so I don’t think these discussions and design choices will ever go away. (At least in Julia you have the choice!)

2 Likes

I have to disagree that this is an unintended result. It is indeed the case that ["a", "b", "c"] does not occur in "b".

My opposition to overtyping of functions is not out of disdain for compiler-checked documentation, but out of disdain for needless loss of generality. Further, I disagree that types are “just for performance”. On the contrary, I believe that types are one of the most powerful features in Julia. They are what allows me to write the same code once, and have it work generally for every similar problem.

Unfortunately, the ability to dispatch functions on types (i.e. strictly typing function arguments) is often misused. A small but informative example: suppose one defines a function ^ that computes an exponent by repeated multiplication. Some authors may needlessly narrow the type of arguments taken to Number. But ^ is by no means restricted to numbers; yet such a definition prevents the same code from being used for matrix exponentiation. I would argue that there is only one sensible restriction of types for ^: Any, as any other restriction will lose generality.

Consequently I think, that for public interfaces at least, developers should make every effort to generalize function argument types as much as sensible. This should be done for the sake of the users, which may have use cases not anticipated by the developers. Return type annotations, on the other hand, are in my opinion merely developer preference.

Every type error that is not caught at compile time is an unintended consequence. I could make up trivial examples, like stuffing a string into a floating point array if some flag is true, or calling a function with differently typed args in the wrong order, or …, and you’d likely rightly say it’s trivial and that you don’t do that. But if you’re working on a large JavaScript project, you’ll see every kind of error, and there’s never enough time to write all the tests, and cover all code paths, etc, etc.

If you’re doing small scale exploratory programming, types and safety are not as important. As your code grows so that you can’t keep it all in your head, types become invaluable.

These are good examples of why restricting types in a method signature is useful and I don’t think anyone disagree with that. However, it is still true that overly constraint the type is a known beginners trap.

Take your floating point array example. You should not allow such insertion to be called on String but in most cases I can think of, you also shouldn’t restrict it to only one leaf type. You should probably use f(::Real) or f(::Number) instead of f(::Float64) or f(::Any). The general idea is that you should pick the right type so that it ensures the necessary safety but allow your code to be used on other user defined types when it makes sense.

2 Likes

Excellent examples! But let me ask a question here. Say you’d written your iterative solver over matrices with over-specific types. Do you really think it’s unlikely that you wouldn’t see how you could generalize the types? Even if you didn’t, someone would, and you could generalize your types or remove them altogether.

For your personal example, all things which define +, -, /, *, and sqrt support an interface or trait. If you can define such a trait in your language, that’s your type constraint. But I’m sure you know that already :wink:

I usually move from concrete to abstract fairly easily, and not as easily the other way. I also tend to be a bit of a slapdash programmer, so having a good static type system and using it keeps me from messing up too much.

This is one of the reasons I like Julia, and look forward to its evolution.

1 Like

If I’m understanding the question properly, the problem is moreso that you can’t have multiple inheritance so in some cases you can’t easily generalize the types. Sometimes what you want to support is an Operator type which could only exist in some library. If you want to <:Operator, then you have to have the library which defines Operator as a dependency. So you have to make a choice:

  1. Hope that everyone subtypes the right things (i.e. make your iterative solver work on Union{Matrix,Function} and tell other libraries they have to subtype Function with their weird overloaded types).
  2. Not try to support any non-Base type.
  3. Leave your function untyped.

For library building, leaving the function as untyped (duck-typing) tends to be the preferred option since it generalizes the applicability. Of course, there are the drawbacks you’ve mentioned (the errors are more obscure since they will always attempt to use the function, and the documentation won’t be as specific), but…

Yes, the “true” answer I think will come from traits. For now, interfaces are informal. However, we have setup in JuliaDiffEq using SimpleTraits.jl (macros for Holy traits) where traits are automatically applied when certain dispatches are implemented (and they are inferred by the compiler), and so ImplementsIterator dispatches are possible using this kind of thing, i.e. you have make a function

f(x::::ImplementsIterator) = ...

and everything about the traits are known at compile time (so no dynamic dispatch).

I think this form of trait-constraints for documentation/errors + automatically/dynamically applied traits (but when the function compiles, so it compiles to fully-performant code as seen by @code_llvm) for ease of use and “auto-extensability” is the middleground. Of course, Julia doesn’t have a base implementation for traits yet so this is still experimental (but very useful! I’m not sure it plays well with pre-compilation either), but I see a bright future.

1 Like

The described operator ^ would only work on types that have multiplication defined. Choosing Any will result in hard to interpret errors further down the stack about * not existing. Similarly, in my example the in function would be safer to use if it could assert that the second argument is some kind of container and the first argument is a valid element of that container. Every other use of the function is obviously an error and should be reported as such. I agree with @ChrisRackauckas that a traits system or more formal interface descriptions may be the solution here.

This kind of problem is why Concepts are now considered for C++, so it would be good for the Julia language to tackle this problem early on. An interface function without type declarations needs very good documentation indeed, otherwise the user is left wondering what kind of arguments the function really expects. Just trying out a type and seeing it doesn’t error out is not proof that everything is OK.

I am a big fan of traits and bringing them to Julia, but that traits are possible does not diminish the fact that they are, presently, not implemented as commonly or as general a way as one would like. The next best option is duck typing.

1 Like

I agree that it is currently the only sane choice. I hope traits will happen soon.

one of the problems beginners like me run into is that if we supply an unsupported type, some obscure error is raised from deep inside the function, claiming that some function does not have some method.

as an experiment, i wanted to alleviate this problem, and cause my functions to test and fail early if the supplied types does not support what i want from them. i tried this:

function myfunc(a)
  if !method_exists(feature, (typeof(a),))
    error("$(typeof(a)) does not have feature")
  end
  ...

i was hoping that Julia could magically eliminate the entire code during compile. but unfortunately it is not the case, some quite heavy code gets injected. a much shorter code, but still some runtime check, and also can’t control the error message:

function myfunc(a)
  @which feature(a)
  ...

maybe there is a neat way to move this check to compile time and thus enabling efficient contract enforcing and early detection of errors.

1 Like

Maybe check this again.

Could you use generated functions?
Not sure if this is recommended, or indeed if it will work.

@generated function myfunc{T}(a::T)
    if method_exists(feature, (T,))
        :(_myfunc(a))
    else
        :(error("$T does not have feature"))
    end
end

function _myfunc(a)
    println("_myfunc called with $(typeof(a))")
end

feature(x::Number) = x*x


myfunc(2)
myfunc("cat")

4 Likes