How is it that new Julia programmers tend to abuse type annotations?

This is pretty offtopic, so just a clarifying remark: I am aware that this is not a simple question. But I am not suggesting that Callable[T, T’] needs to be the type in the sense of the Julia type system. I am suggesting that it should be possible to declare that a function satisfies a certain “Trait”, such that all methods have to have a signature that conforms to that “Trait”. My impression is that one could design this system to be orthogonal to multiple dispatch.

In my mind, the closest analogue to Julia’s situation is template metaprogramming in C++. C++ got Templates in 1991. It got Concepts and Constraints in C++20, so that’s a 30 year gap to address this issue.

My overall point is: People use the type system in other languages to help them make sure their code is correct and works as assumed (this thread is full of people confirming this). When transitioning to Julia, the manual tells them to essentially “just stop caring about that stuff”. This also occurs when people transition from Python and object oriented programming. Object oriented programming can do quite a bit of this “encoding of assumptions” in a dynamic context.

We simply don’t have a good answer here. We can’t tell them “you don’t need object oriented programming, or concrete typing, you can achieve the same things in Julia using features X,Y,Z”. Because there are things that these systems do, that Julia simply lacks. That’s why we don’t have tutorials or manuals or style guides for how to achieve these things. Because the answer is always some unsatisfactory variation of “document things well, use tests and pray you didn’t make mistakes”.

2 Likes

There seem to be several projects trying to bring some of the advantages of statically typed languages to Julia, so I would like to know if there are any websites that explain and track their status in a similar way to the “are we xxx yet?” websites.

1 Like

What’s a trait?

Can you open a new topic, to avoid making the discussion here even longer and less focused? You can tag me there if you wish.

No need, you can just look at @FHell’s old posts to see more of their opinions on this topic, e.g.: Protocols/Interfaces/Traits... or: The "what to implement problem"

I think it’s time to put a close timer on this topic. The original topic has long since been eclipsed by a meandering debate about the general utility of typing and of how many additional constraints could be put into the type domain (ala C++), which is a discussion that will never end.

5 Likes

What I find in the literature is that argument type management matters a lot for the compiler to improve Performance:
A1.- Type-stability

In order for the Julia compiler to compile a specialized version of a function for each different type of argument, it needs to infer, as best as possible, the parameter and return types of all functions. If it doesn’t do this, the speed of Julia will be hugely compromised. In order to do this effectively,

the code must be written in a way that it is type-stable.

Sengupta, Avik. Julia High Performance: Optimizations, distributed computing, multithreading, and GPU programming with Julia 1.0 and beyond, 2nd Edition (English Edition) (p. 41). Packt Publishing. Edición de Kindle.

A2.- “You will work with a function for calculating the roots of a quadratic equation:”

ax² + bx + c =0

…The reason for the slow performance of the quadratic1 function is that it is not type-stable. In Julia,

*** this means that the compiler is generally unable to determine the types of all variables used in the body of the function.***

There are two possible reasons for this. One is that the type of the variable changes during execution of the function, and the other is that there is some obstacle preventing type inference from working (at least at the current stage of the Julia compiler).
In this recipe, we discuss the second problem, as it is subtler. The first one (changing of the variable type) is much simpler and is explained in detail in the Julia manual at…

Bogumił Kamiński, Przemysław Szufel. Julia 1.0 Programming Cookbook
Copyright © 2018 Packt Publishing.Over 100 numerical and distributed computing recipes for your daily data science workflow

B.- Chapter::Types, Type Inference, and Stability.

Julia is a dynamically typed language. Unlike languages such as Java or C, the programmer does not need to specify the fixed type of every variable in the program. Yet, somewhat counterintuitively, Julia achieves its impressive performance characteristics by inferring and using the type information for all the data in the program. In this chapter, we will start with a brief look at the type system in the language and then explain how to use this type system to write high-performance code.

Sengupta, Avik. Julia High Performance: Optimizations, distributed computing, multithreading, and GPU programming with Julia 1.0 and beyond, 2nd Edition (English Edition) (p. 33). Packt Publishing. Edición de Kindle.

C.- Fixing type instability.

Now that we can recognize type-unstable code, the question arises: how can we fix code such as this?. There are two obvious solutions.

One would be to write separate versions of the pos function for different input types. So, we could have a version of pos for integers and another for floating points. However, this would cause instances of repeated, copy-pasted code. Also, there would not just be two such instances; there would be copies for Float32, Float64, Int32, Int64, and so on. Further, we would have to write a new version of this function for all the new numeric types that were defined. It should be obvious that writing generic functions that operate on a wide variety of related types is really the best way to get concise and elegant Julia code.

The second obvious solution is to branch out the input type within the generic function. So, we could write code along these lines, as follows:…

Sengupta, Avik. Julia High Performance: optimizations, distributed computing, multithreading, and GPU programming with Julia 1.0 and beyond, 2nd Edition (English Edition) (p. 42). Packt Publishing. Edición de Kindle.

1 Like

Am I misreading these comments? Haskell’s map and fmap allow the input and output element types to differ e.g. fmap ord "a2b3" (funnily enough, Julia’s map(f, s::AbstractString) more strictly requires f to return AbstractChar), it’s just the functor (the elements’ container) that must be the same. I don’t think we’d want to emulate that. Even if the input function could have specified input and output types (easy type stability!), it is still useful for the container types to differ. The dest in map!(log2, dest, 1:3) can’t possibly be a UnitRange, and allocating another instance of dest’s type to store 1,2,3 would be superfluous, especially if dest is a wrapper like SubArray.

Seems we are aligned here. The type in Haskell is fmap :: Functor f => (a -> b) -> f a -> f b, i.e, the element type can certainly vary and is decided by the function you map a -> b. What cannot change is the type of the container, i.e., the container f must be the same in f a and f b.
Your range example is very good in that map(log, 1:10) could not possibly return the same container type. Nevertheless, this implies that you cannot assume much about what this function will return: You know the element-type Float64, but not what container it will be, i.e., in an interface like in scala where its an something like an Iterable[Float64] you could not do any indexing or other vector operations on it without an explicit collection step.
In the end, it’s always a compromise between genericity and composability and Haskell is very precise and imho often correct on this end. I.e., with other choices generic code often becomes less composable, more verbose or harder to get correct.

In-place functions like map! conventionally return dest and map falls back to Array via similar. The former is reliable, but similar is intended to have methods that customize the output array type e.g. BitArray in, BitArray out. You are ultimately right because there is no requirement that the output type be inferrable from method signatures alone. Instead of a default type or the input type, the output type can be anything the method body derives from the input type, which lets input functions not need declared signatures (something I do appreciate for simple runtime dispatch and heterogeneous collections). I think this can technically still occur in a statically typed language, provided the inference is successful at compile-time (type stability), but they don’t seem to take type inference or generic functions this far.

Could you explain the meaning of composability here? I’m thinking of the wrong thing because in my mind, Julia’s generic functions are a tad more “generic”, and this allows “composing” more mixes of input and output types. I do see how output types being harder to infer and less guaranteed at compile-time can make code harder to pin down. For all the generic type-stability practices and customizable output types, in a sense we do let type inference take the wheel at some point.

Ok, let’s see if I can come up with an example illustrating my point

# Which of these snippets will work?
step(1:10)
step(2 .* (1:10))
step(log.(1:10))
...

In Haskell/Scala/Rust the compiler could probably tell you in most of the cases. On the other hand, you might not be allowed to execute some of the lines – even though they would work, i.e., assuming that broadcasting would be typed as broadcast(f::Callable[A, B], itr::Iterable[A])::Iterable[B] you would not be allowed to write step on a broadcasted object in any case.
On the other hand, assuming that the container type does not change, properly typing these examples would be rather hard, i.e., how would you statically catch that the last version cannot work?
I think of genericity of the code at it’s level of abstraction, i.e., what can/cannot be assumed when using a generic function – is * a monoid operation, can fmap be used on functions? With composability, I mean the ability to compose different abstractions such that the resulting program actually works as intended. Here, Julia is great even though it can break in unexpected ways – it can also work in unexpected ways though as the compiler is not always guarding/annoying you!

2 Likes

Yeah that aligns with what I meant by “harder to pin down”. Those are still type-stable, so technically it could be statically caught with enough type inference. But the compiler happily compiles f(x::UnitRange{Int}) = step(log.(x)) to always throw a runtime error; the Union{} return type in @code_warntype f(1:3) hints at this, but doesn’t report step(::Vector{Float64}) throwing a MethodError. Putting the compiler catching things ahead of time aside, Julia methods could be designed with more predictable return types more often (docstrings tend to point out when they are), but people seem to embrace the flexibility.

1 Like

Guess in the end, it’s again the trade-off between static guarantees and allowed code. Depending on the type system, a static language would not allow you to even try running your nicely composed code whereas in dynamic languages you can find out at runtime if it worked or not. Some examples that did work in Julia, but are unlikely to have been found in static languages are certainly uncertainty propagation or auto-diff through ODE solver (it works now in Torch etc for the few solvers they have ported).
Maybe we should try to find another solution to the correctness challenges than relying on static typing (which can be severly restrict the code you are allowed to run), e.g., AbstractDifferentiation makes it very easy to test against numerical integration.

Could a solution like the one the Python/TypeScript developers have come up with alleviate the situation?

What does that solve? From my understanding, code in Python is usually much less generic than Julia and types are not really used for much currently.
Don’t know much about TypeScript. Can you share some details here?

Type hints in Python and TypeScript are simply hints that can be checked by a type checker, but unlike Julia, they are not useful for code execution. I think they try to solve the two main problems of this thread: they help document functions, and they can improve the correctness of a function with the type checker.

I don´t know if the next definition is a good definition:
A function is is inherently type unstable if the return type indeed depends on the value, not the type, of its arguments.”

This topic was automatically closed after 42 hours. New replies are no longer allowed.