My mental load using Julia is much higher than, e.g., in Python. How to reduce it?

showerror(MethodError), REPL.Completions, and which all implement some portion of this, so I think it’s reasonable. Maybe more work in this direction could be added as part of ? help?

3 Likes

This is pretty close to what I would do and indeed seems to be what Julia wants me to do ( type system and multiple dispatch ). Rather than behaviours I would tend to write pure functions which I would describe more as a mapping/transformation than a behavior. Which Julia also seems to want me to do ( iterators and broadcasting ).

There are packages out there to complete the story a bit more for Types and Behaviors ( e.g. traits, structural inheritance ) and also packages to support Transformations ( e.g. lazy functional stuff and “currying” ). I’m hoping both approaches make their way into a more officially supported form. I do appreciate that you take your time and not rush into supporting stuff before it really fits.

Not flippant at all, this is good advice. When I have taken this approach I’ve often been surprised by how optimal it turns out. Compilers are really magic and often can do a much better job at optimizing code than I can by thinking about it too hard.

6 Likes

I think starting with abstract types is a mistake: most of the time functions work best with “interfaces” not types, that is, it’s not about the type, but about which functions the type overrides. I’ve had to go back and delete type hierarchies several times when I realised that what started as an abstract type was better suited as a “trait”.

In other words, I suggest starting projects with concrete types and functions, and only add abstract types when needed, and this should be motivated by software needs, not mathematical definitions.

9 Likes

I had a similar experience. When I started programming Julia, I used to build elaborate type hierarchies like

abstract type AbstractGizmo end
abstract type AbstractSpecialGizmo <: AbstractGizmo end
function f(g::AbstractGizmo)
    do_stuff(g)
end

This gave me the illusion of doing something clever until I encountered things which wanted to be two or more kinds of abstract things at the same time. Now whenever I refactor code, I tend do

using ArgCheck

abstract type AbstractGizmo end
# not exported or part of the API, just for internal use

"Does the argument support the Gizmo interface?"
is_gizmo(::AbstractGizmo) = true 
# could be a singleton type for traits, or just a boolean for testing
# user defines it for user-provided types

function f(g)
    @argcheck is_gizmo(g) "Argument does not support the required interface."
    do_stuff(g)
end
10 Likes

My big revelation along these lines recently is to encode some things into type parameters, which can hugely improve performance in some situations. When I first started, I encoded some Boolean information using BitArray stored in the fields of a type. However, I later split this single BitArray information off into an integer value (to store the bits) and some type parameters to store some other specialized type information, which I originally tacked onto the end of the BitArray. Because these last two bits of information were used differently than the other bits to determine program logic for an entire sub-class of objects, encoding them into the type parameters instead helped improve the performance by simplifying the compiled code. However, I think it was good to first get started with building up a working prototype, which gives me an idea of what kind of program structure was needed, and then to optimize it into better Julia code afterwards, when I had a stronger vision of what the requirements were for making the type system better.

However, I think there is some kind of balance you want to strike with this, because it is actually better to work out these kinks at the beginning as soon as possible, so that you don’t have to go back and rework a large amount of code. So, I think when building up your ideas like this, you have to plan ahead a bit and gradually make sure you are satisfied with your initial type setup, so that you don’t have to revisit a large amount of code when you are fixing things with modifications.

So it is a good idea to start with the type system first, build up things intuitively and conceptually, but to also make sure that as you progress along that you are building up things from a central kernel of basic functions and types which build on each other, and to wait with implementing the outer library which rests on the kernel until later, as you progress and make optimizations to this inner kernel of functionality. This way, if you think of building things up like this, you can both start with intuitive ideas which you can revise and optimize into a better structure before you are confident to build up the rest of the library API, so that it doesn’t have to be rewritten over and over with breaking changes. I pretty much agree with all the other points raised here, but I just wanted to add this perspective about an inner kernel and outer library also.

I was under the impression the establish pattern for traits was something like this:

abstract type Iteration end
struct HasIterate <: Iteration end
struct NoIterate  <: Iteration end
Iteration(t::Type) = hasmethod(iterate, Tuple{t}) ? HasIterate() : NoIterate()
Iteration(::T) where T = Iteration(T)

somefunc(x) = somefunc(Iterate(x), x)
somefunc(::HasIterate, x) = ...
somefunc(::NoIterate, x) = ...

I got that impression from here, but maybe I’m not doing it right.

I have observed this also. My experience is that type annotations only slow things down, which was surprising. My theory is that types are always enforced at runtime (i.e. invoke a type check), but all functions are just compiled for given arguments as needed. Not saying you should never check those types, but you save time by just leaving it alone and letting the JIT do its thing. You just loose out on useful error messages.

As I said in the comment above, it depends if you want to dispatch on the trait, or not. If yes, use a type, if not, you can use a value. I don’t know if hasmethod is efficient though, I would just use a default fallback.

1 Like

There is also https://github.com/rafaqz/Mixers.jl for field and parameter mixins. It’s a light weight alternative for inheriting fields manually, for use with traits or regular inheritance.

3 Likes

With the benefit of four years’ experience, do you feel this problem is satisfactorily solved now? If so, how?

1 Like