Add type mixins to overcome subtype limitations

Mixins, concerns, whatever you want to call them, the ability to share many fields between types is an all too common problem.

Obviously one way to solve this would be to use macros and then put some lipstick on the pig.

However, I think this lacks elegance and probably leads to unneeded performance drains.

What can we do to fix this?

edit: for future reference, the following discourse post covers similar topics:

1 Like

Why would you think it has performance problems? It would all be done at compile time.

1 Like

Because when you incorporate something directly into a language, thereā€™s usually a way to optimize it

Iā€™d say that Chris is 100% right about this one. Just think about immutable types that get inlined ā€“ how are you going to pack more bits into the same amount of memory because you have mix-ins?

Put another way: what specifically is going to change about the memory layout of a type with a mix-in? Formulating the exact layout optimizations you have in mind is a great way to convince people to make a change like this.

I think you guys (see Julians) get bogged down by details too often and lose sight of the big picture.

Instead of discussing pros and cons of ideas, you focus on minutia like sentence structure & short-term gains.

What Iā€™m talking about is a way to make code DRYer and more maintainable.

I agree with you that it would be a good idea to have something for this. But I wouldnā€™t expect any performance gains: it would likely just be syntactic sugar to essentially do what the macros do. You can probably even implement a package for interfaces using macros if youā€™d like, and submit that as what Base should look like.

1 Like

Is the inlining of immutables aided by Julia compiler or is it mostly automatic LLVM optimizations?

I am 90% confident that Julia specifies the memory layout of types like isbits immutables as part of its current semantics (in order to allow FFI, for example) and just informs LLVM that types have the layout that Julia defines. Hopefully someone like Jameson will correct me if Iā€™m getting that wrong.

1 Like

You prompted that digression by pointing to potential performance gains, which are clearly not applicable because macros are called at compile time and therefore give the same runtime performance as builtin syntax.

If your point is about elegance, then just say it. The best way to improve things is to describe existing macros and what you consider as their limitations. Then propose better solutions (e.g. syntax).

Note that thereā€™s an intermediate solution between new syntax and macros provided by packages: thatā€™s macros provided in Julia Base. For example, enums can be considered as a fundamental feature, and yet they are provided via @enum rather than via a special syntax, just because the macro offers everything we need. Often such macros can be first implemented in packages, and once the design has stabilized and many users are happy with them they can be moved to Base (at least when they correspond to a common enough need).

4 Likes

Note that this doesnā€™t really apply to Julia. Essentially everything available to Base is available to package developers. Things donā€™t have to be built into Base (or be written in C) to be fast, and syntax doesnā€™t even have to be declared in Base to exist (in macros). So you can make your own highly performant syntax and just tell people to put a macro around the type definition:

@inheritable immutable SuperType
  a
  b
end

# Now inherit the fields
@inherits immutable MyType <: SuperType end

Implement that and now you have a form of inheritance of fields which is as optimized as it would be if it was in the Base library. Lots of basic things, like the advanced arrays and anonymous functions, start off in packages.

2 Likes

I think youā€™ve raised an important point here and one that deserves a good answer. Itā€™s been a while since I looked at the manual but, as far as I know, we donā€™t have any real documentation that addresses this ā€“ which is particularly likely to cause confusion when everyone is to the Java-esque solution.

Generally speaking, Julia has been used quite widely and in very large (~100kloc) codebases, so itā€™s unlikely that itā€™s missing obvious features that would make this stuff very difficult. In most cases, it turns out that Julia handles this stuff fine ā€“ but perhaps take a different approach compared to other languages.

In this case, Julia uses ā€œhas-aā€ rather than ā€œis-aā€ relationships. Trivial example:

type Noise
  sound::String
  loud::Bool
end

makenoise(n::Noise) = println(n.loud ? uppercase(n.sound) : n.sound)

type Dog
  wetnose::Bool
  noise::Noise
end

Dog(wetnose) = Dog(wetnose, Noise("woof", true))

type Cat
  longwhiskers::Bool
  noise::Noise
end

Cat(longwhiskers) = Cat(longwhiskers, Noise("purr", false))

This approach is going to be weird for those whoā€™ve used more classic inheritance models, but itā€™s arguably more general and powerful. You can inherit behaviour and fields from multiple sources and combine them with total control. Having to explicitly forward methods is a line or two of extra boilerplate (annoying I know :slight_smile: ) but thatā€™s relatively minimal. Hereā€™s a post I wrote where I go over a more complex architecture in excruciating detail.

It may take a while to get used to this but I think it should address your underlying issue. Itā€™s been a powerful way to handle all the cases Iā€™ve come across. If you come across a case where that pattern isnā€™t working for you, weā€™d be more than happy to help figure out either how to make it work or what improvements the language needs to support it.

12 Likes

You may be right in this case but itā€™s not as obvious as you say. There are many things which can be implemented via macros but not made fast with them; generated functions being a prime example. Itā€™s plausible that more optimisations to memory layout may be possible with more static information.

Generally, I think the community should avoid piling on over things like this. Perhaps in an ideal world the question should have been phrased as asking for help rather than as feature request, perhaps it confuses means with ends, perhaps it includes extraneous (or incorrect) detail. But phrasing these questions ā€œcorrectlyā€ when you donā€™t know Julia like the back of your hand is hard and people arenā€™t always going to get it right. Itā€™s easy to attack feature requests but much more helpful to try to understand the underlying issue.

5 Likes

@MikeInnes Thereā€™s no problem with making feature requests that donā€™t master all the implementation issues at stake. I was just replying to criticism from the OP that Julia people were talking too much about details.

It could also be helpful to have an abstract helper type for all types that have a noise field:

abstract MakesNoise

volume(x::MakesNoise) = x.noise.loud ? 20 : 10

make_some_noise(x::MakesNoise) = play(Sound(x.noise.sound), volume(x))

Then you could make Dog <: MakesNoise and Cat <: MakesNoise and ā€œpaintā€ noise-related method onto those types. The main limitation here is that types canā€™t inherit from multiple abstract types ā€“ which is a limitation weā€™ve been aware of for some time.

The current high-level language design situation weā€™re in is that we need some subset of the following features:

  • abstract multiple inheritance
  • interfaces
  • protocols
  • traits
  • delegation
  • field sharing

There are all in roughly the same general area of language design: shared functionality and structure. The hard part is figuring out how to slice up this design space into a few features that are simple to use and understand, nicely orthogonal, have efficient implementations, and donā€™t do anything unfortunate to the way people use the language. This is a hard design problem and not one that can be solved piecemeal ā€“ if we choose one feature and add it without figuring out the rest of the picture, then the end result will almost certainly not cohere as well as we would like. And once youā€™ve added a feature, itā€™s pretty hard to take it back since thousands of people will already be using it. So for now, since people are getting by well enough with macros and multiple dispatch, weā€™ve decided that this design problem can be tackled after we get a 1.0 version of Julia out the door.

12 Likes

I donā€™t know what youā€™re definition of ā€œprotocolsā€ is but I think traits (as defined in Scala) cover all of this list I think.

A thorough proposal explaining how that would work would be helpful.

As an aside, I would like to suggest that this not be the case. I think Julia, as a high-level language, should be free to pack its types in the most efficient manner possible in order to exploit the memory hierarchy. This affects immutables, but also tuples etc.

I think choosing the ā€œCā€ style layout should be an opt-in, not the default. Interop with C is important but it also prevents certain optimizations, some of which could be a distinguished advantage for Julia. Structure layout affects everything from memory size, memory/cache efficiency to parameter passing at the instruction level.

I think a big issue Julia faces in this space is that ā€˜interfacesā€™ are purely ā€˜conventionā€™; thatā€™s really what a lot of this discussion boils down to. My biggest fear wrt this is that you end up with a mixture of the following problems which affect large projects in the below languages:

  • C++: Templates, and their requirements, are still only specified by ā€œconventionā€. For example, you assume that a parameter is an iterator and then try to apply iterator operations to it; if it doesnā€™t satisfy them, you will get an error; the unfortunate problem is that to novices, the error is frequently completely obscure and usually comes deep within some other framework or the standard library. However, at least it is caught at compile time.
  • Python: Refactoring large python code bases is a nightmare, in my experience. One of the reasons is that if an interface needs to change, there is no easy way to allow the compiler to tell you ā€œHey, youā€™re calling this function incorrectlyā€; you will only end up with an error at runtime.

These problems are not going to surprise anyone, and both are recognised and addressed by the community; C++ has a Concepts TS. Python 3 introduced function type annotations, and in fact the just-released Python 3.6 has added variable annotations; now a tool - like, for example, mypy - can at least do some analysis and help you catch errors which can be easily diagnosed before runtime.

Iā€™ve been reading some of the related threads on GitHub and saw one proposing, what are effectively Haskellā€™s type classes - or C++ concepts, if you like (I donā€™t know Scala so canā€™t comment on Scala protocols). This could address some of the concerns voiced here, and on other github threads. However, before such a facility is available, I think Julia could benefit even from a simpler mechanism - something akin to the notion of ā€œpure virtual functionsā€, which could enforce certain requirements being satisfied.

I appreciate the many benefits of a system where abstract classes have no state, and I also read a related discussion on Github from a little while back proposing adding attributes to abstract classes. Introducing state to abstract classes is fraught with a number of problems (see C++ multiple/virtual inheritance), but leaning back on the ā€œpure virtualā€ notion, I think a near-term, middle of the road solution would be to allow ā€˜abstractā€™ attributes in abstract classes. Those have no state; they are again just requirements, and could be implemented using an actual attribute (state) in concrete types, or if the proposals ever come around, using the ā€œgetfieldā€ functionality Iā€™ve seen @jeff.bezanson and others mention. (As an aside, now that Iā€™ve written this down, I feel like Iā€™ve seen this before. Itā€™s been a few years since Iā€™ve done serious Matlab development but I believe the abstract attribute was something that was added to the ā€œnewā€ object model when it was introduced around 2007).

Kind regards,

Tom

I see similar posts to this all the time (i.e. workaround for traditional inheritance) and was wondering if we could consolidate it into the docs somehow.

I feel like each post starts over from scratch with no after-v1.0 end goal in sight

1 Like