[interest] Type Aspects and compile-time constraints

Hi everyone, I would like to introduce a project I'm working on and a mechanism that emerged from the projects needs and, as far as I could feel, may be useful in a general case. By lack of words (let say), I named this Type Aspects, as an analogy to Type Traits. The project I'm working on, for quite a while now, is about creating a statically, strongly typed, compile-time checked and heavily simplified interface to OpenGL. I know that "Boost.OpenGL" is a subject that has been discussed many times on this list and that every time the outcome of the discussion is that Boost is not going to support a mere wrapper around some existing API. Well, I perfectly understand this and I'm not looking for the possibility to have this included in Boost. The project itself and its functional goal isn't actually the subject of my post, but it is a good introduction to type aspects, as it is what led me writting this as a general supporting mechanism. So, to introduce the idea, if some people do not know OpenGL, it is a typical C API, full of enumerations (well not even enumerations, integers preprocessor constants), loosely typed, and full of bloated function names with fixed number of parameters of various flavors. Obviously, every single function call takes an average of two of those constants as parameters. And obviously most combinations of values are leading to undefined operations, whithout having anyone complaining about anything, except a black window on your screen and a poor-man error code whenever there is one. Probably the most hideous nightmare you could ever have to use as a programmer. That was my observation. So I decided to take all these constants, that any OpenGL programmer is usually giving directly to feed the function calls, as parameters, to take all these functions, wrapp them all, rename related functions to use nice C++ overloading... but above all, convert these constants as types. This way I was able to write compile time constraints, checking the combination of constants provided to these functions. The goal was (and I must admit that it's working pretty much fine), that I would certainly be able to write the constraints described in the OpenGL specifications in term of templated C++ type constraints, making good use of static assertions with nice messages referring to the spec itself, and of metaprogramming techniques to achieve this. Then it appeared, after struggling a while trying to abstract all the data in use in OpenGL, that I could extract some fundamental properties of all these types and that the constraints described in the specifications would be experessed as simple and clear constraints on these properties. That's what I defined as Type Aspects. Let say OpenGL is able to handle 10 different type of textures. These types are different by the way color components are packed together, by their order, by their precision or numeric type (floating point or integral). All these are aspects of the texture type. Some OpenGL function might then be used to load data to a given texture, but depending on the texture type, it will only accept a certain type of source data. For example, integral textures only accept integral data, whereas floating point textures accept any data. This would be implemented as a simple constraint checking that the "Numeric" aspect of the source data type is compatible with the "Numeric" aspect of the texture type. Moreover, this also allows the user to provide a limited set of these aspects that he desires to have, and not others that he doesn't care of, and the compiler would be able to resolve the more relevant type to use depending on the use case. Type Aspects, as I defined it for the use in my projects, are like axises describing a type, every axis taking a value in a given set of possible values. This is not limited: as these values are types as well, they can also have aspects and be described. As an example that's what I did to describe the "Numeric" aspects of OpenGL data. Numeric aspect takes a value in a given set of predefined numeric archetypes. Every single archetype then has some values the three aspects describing a number type: its signness (signed, unsigned, any), its integralness (floating, fixed point, integral, boolean, any), and its width (number of bits/bytes, any). Well to conclude, I can see that this is certainly not related to OpenGL anymore and that this is a general mechanism that can be used to describe types and create relationships between types. I don't know if it is a subject that already has been discussed or if there's anything like that in Boost already (I don't think so but I don't know all the Boost libraries by heart). Do you see any interest in this kind of thing? Do you think that this could be something that worth working on deeper? TL;DR: 1. OpenGL sucks, I implemented compile-time constraints wrapping the functions calls. 2. Thinking about compile-time constraints made me qualify the types in use with a general mechanism describing the type's "aspects". 3. Aspects are axises and a given set of predefined values on these axises using which a type and its properties can be described. 4. Constraints can then be easily written as MPL checks on aspects axises and values as long as type aspects are formalized. 5. Does it already exist? Is that interesing? Should I try to go deeper in the abstraction? Kind regards, -- Beren Minor

On Wed, Feb 8, 2012 at 2:44 PM, Beren Minor <beren.minor+boost@gmail.com>wrote:
Hi everyone,
[...]
TL;DR:
1. OpenGL sucks, I implemented compile-time constraints wrapping the functions calls. 2. Thinking about compile-time constraints made me qualify the types in use with a general mechanism describing the type's "aspects". 3. Aspects are axises and a given set of predefined values on these axises using which a type and its properties can be described. 4. Constraints can then be easily written as MPL checks on aspects axises and values as long as type aspects are formalized. 5. Does it already exist? Is that interesing? Should I try to go deeper in the abstraction?
Looks to me like the traits idiom (e.g., std::iterator_traits). Perhaps a concrete example will illustrate your point better? - Jeff

Looks to me like the traits idiom (e.g., std::iterator_traits). Perhaps a concrete example will illustrate your point better?
It's close to what std::iterator_traits defines but generalizes it. Let's get more practical and take my "Numeric" aspect I was talking about above. I want to describe number related types over a given number of properties. Let say that signness, integralness and width are properties that matter for numeric types. These aspect axises could be defined in a similar way by givin them an "Aspect" name, and a given set of values: META_ASPECT_DECLARE(sign, (signed_)(unsigned_)(any)) META_ASPECT_DECLARE(integral, (floating)(fixed)(integral)(boolean)(any)) META_ASPECT_DECLARE(width, (one_bit)(two_bits)(three_bits) [...] (eight_bytes)(any)) This defines three aspects axises, sign, integral and width which could take the given values. This is a slightly simplified version of things I'm actually using. The detail of the macros isn't very relevant. Now let's define the aspect of a numerical type: template< typename Type, typename Sign, typename Integral, typename Width > struct numeric_type_aspect { typedef Type type; typedef Sign sign; typedef Integral integral; typedef Width width; }; #define DECLARE_TYPE(type_m, sign_m, integral_m, width_m) \ template< > struct numeric_type< type::type_m > { \ typedef numeric_type_aspect< type::type_m, \ sign::sign_m, \ integral::integral_m, \ width::width_m > aspect; \ }; \ typedef numeric_type< type::type_m > type_m; DECLARE_TYPE(signed_, signed_, any, any) DECLARE_TYPE(unsigned_, unsigned_, any, any) DECLARE_TYPE(integral_, any, integral, any) DECLARE_TYPE(signed_integral, signed_, integral, any) [...] DECLARE_TYPE(uint1_, unsigned_, integral, one_bit) DECLARE_TYPE(uint2_, unsigned_, integral, two_bits) DECLARE_TYPE(uint4_, unsigned_, integral, four_bits) DECLARE_TYPE(float_least10_, signed_, floating, eleven_bits) #undef DECLARE_TYPE This is purely abstract types for my internal constraint checking but this could be done more the same way when describing any type. Here I've defined a full list of numeric types, more or less defined (signed_ is a vaguely defined signed_ type, whereas uint4_ is a fully defined unsigned integral type on 4bits) that are described on a given set of axises (three plus one which is the actual type itself). The fact that the type itself is a description axis allows me to use it to describe the numeric aspect of some more general data type. For example the way OpenGL accepts data to load from host memory to the GPU textures can be defined using a numeric aspect, as long as with some other: META_ASPECT_DECLARE(packing, (one_in_one)(two_in_one) (three_in_one)(four_in_one)) META_ASPECT_DECLARE(order, (forward)(reverse)) template< typename Type, typename Numeric, typename Packing, typename Order > struct data_type_aspect { typedef Type type; typedef Numeric numeric; typedef Packing packing; typedef Order order; }; #define DECLARE_TYPE(type_m, numeric_m, packing_m, order_m) \ template< > struct data_type< type::type_m > { \ typedef data_type_aspect< type::type_m, \ fcmn::numeric::numeric_m, \ fcmn::packing::packing_m, \ fcmn::order::order_m > aspect; \ }; \ typedef data_type< type::type_m > type_m; DECLARE_TYPE(gl_unsigned_byte, uint8_, one_in_one, forward) DECLARE_TYPE(gl_byte, int8_, one_in_one, forward) DECLARE_TYPE(gl_unsigned_short, uint16_, one_in_one, forward) DECLARE_TYPE(gl_short, int16_, one_in_one, forward) [...] DECLARE_TYPE(gl_unsigned_int_24_8, ufloat32_, two_in_one, forward) DECLARE_TYPE(gl_unsigned_int_10f_11f_11f_rev, ufloat32_, three_in_one, reverse) DECLARE_TYPE(gl_unsigned_int_5_9_9_9_rev, ufloat32_, three_in_one, reverse) DECLARE_TYPE(gl_float_32_unsigned_int_24_8_rev, float32_stride32_, two_in_one, reverse) #undef DECLARE_TYPE The numeric aspect of every data type is one of the numeric type defined above, which has all its properties well defined. Then, the packing aspect can describe how colors components are packed in the source data (we can have a set of integers on 32bits but do we have one, two, three or four components packed in it?), then the order aspect describes how these components are ordered. Then constraints can be written by querying these aspects (which is done in a generalized way, and using boilerplate code generated by the macros) and using MPL to generate boolean conditions: template< typename DataType, typename GroupType > struct integral_check { typedef bm::and_< fnum::integral::is_integral< typename fcmn::get_numeric< GroupType >::type >, fnum::integral::is_not_integral< typename fcmn::get_numeric< DataType >::type > > group_is_integral_but_data_is_not; typedef bm::not_< group_is_integral_but_data_is_not > type; static_assert(type::value, "GroupType is not compatible with DataType"); static_assert(type::value, ""); static_assert(type::value, " [3.7.2 Transfer of Pixel Rectangles]"); static_assert(type::value, " [3.9.3 Texture Image Specification]"); static_assert(bm::not_< group_is_integral_but_data_is_not >::value, " - GroupType is integral but DataType is not."); }; template< typename DataType, typename GroupType > struct component_check { typedef bm::and_< fcmn::component::is_depth_stencil< GroupType >, fcmn::packing::is_not_two_in_one< DataType > > group_is_depth_stencil_but_data_is_not_two_packed; typedef bm::not_< group_is_depth_stencil_but_data_is_not_two_packed > type; static_assert(type::value, "GroupType is not compatible with DataType"); static_assert(type::value, ""); static_assert(type::value, " [3.7.2 Transfer of Pixel Rectangles]"); static_assert(type::value, " [3.9.3 Texture Image Specification]"); static_assert(bm::not_< group_is_depth_stencil_but_data_is_not_two_packed >::value, " - GroupType is depth_stencil but DataType is not two_in_one packed."); }; -- Beren Minor

On Wed, Feb 8, 2012 at 3:58 PM, Beren Minor <beren.minor+boost@gmail.com>wrote:
Looks to me like the traits idiom (e.g., std::iterator_traits). Perhaps a concrete example will illustrate your point better?
It's close to what std::iterator_traits defines but generalizes it. Let's get more practical and take my "Numeric" aspect I was talking about above.
I want to describe number related types over a given number of properties. Let say that signness, integralness and width are properties that matter for numeric types. These aspect axises could be defined in a similar way by givin them an "Aspect" name, and a given set of values:
META_ASPECT_DECLARE(sign, (signed_)(unsigned_)(any)) META_ASPECT_DECLARE(integral, (floating)(fixed)(integral)(boolean)(any)) META_ASPECT_DECLARE(width, (one_bit)(two_bits)(three_bits) [...] (eight_bytes)(any))
This defines three aspects axises, sign, integral and width which could take the given values. This is a slightly simplified version of things I'm actually using. The detail of the macros isn't very relevant.
Now let's define the aspect of a numerical type:
template< typename Type, typename Sign, typename Integral, typename Width > struct numeric_type_aspect { typedef Type type; typedef Sign sign; typedef Integral integral; typedef Width width; };
#define DECLARE_TYPE(type_m, sign_m, integral_m, width_m) \ template< > struct numeric_type< type::type_m > { \ typedef numeric_type_aspect< type::type_m, \ sign::sign_m, \ integral::integral_m, \ width::width_m > aspect; \ }; \ typedef numeric_type< type::type_m > type_m;
DECLARE_TYPE(signed_, signed_, any, any) DECLARE_TYPE(unsigned_, unsigned_, any, any)
DECLARE_TYPE(integral_, any, integral, any) DECLARE_TYPE(signed_integral, signed_, integral, any)
[...]
DECLARE_TYPE(uint1_, unsigned_, integral, one_bit) DECLARE_TYPE(uint2_, unsigned_, integral, two_bits) DECLARE_TYPE(uint4_, unsigned_, integral, four_bits) DECLARE_TYPE(float_least10_, signed_, floating, eleven_bits)
#undef DECLARE_TYPE
This is purely abstract types for my internal constraint checking but this could be done more the same way when describing any type. Here I've defined a full list of numeric types, more or less defined (signed_ is a vaguely defined signed_ type, whereas uint4_ is a fully defined unsigned integral type on 4bits) that are described on a given set of axises (three plus one which is the actual type itself).
The fact that the type itself is a description axis allows me to use it to describe the numeric aspect of some more general data type. For example the way OpenGL accepts data to load from host memory to the GPU textures can be defined using a numeric aspect, as long as with some other:
META_ASPECT_DECLARE(packing, (one_in_one)(two_in_one) (three_in_one)(four_in_one)) META_ASPECT_DECLARE(order, (forward)(reverse))
template< typename Type, typename Numeric, typename Packing, typename Order > struct data_type_aspect { typedef Type type; typedef Numeric numeric; typedef Packing packing; typedef Order order; };
#define DECLARE_TYPE(type_m, numeric_m, packing_m, order_m) \ template< > struct data_type< type::type_m > { \ typedef data_type_aspect< type::type_m, \ fcmn::numeric::numeric_m, \ fcmn::packing::packing_m, \ fcmn::order::order_m > aspect; \ }; \ typedef data_type< type::type_m > type_m;
DECLARE_TYPE(gl_unsigned_byte, uint8_, one_in_one, forward) DECLARE_TYPE(gl_byte, int8_, one_in_one, forward) DECLARE_TYPE(gl_unsigned_short, uint16_, one_in_one, forward) DECLARE_TYPE(gl_short, int16_, one_in_one, forward)
[...]
DECLARE_TYPE(gl_unsigned_int_24_8, ufloat32_, two_in_one, forward) DECLARE_TYPE(gl_unsigned_int_10f_11f_11f_rev, ufloat32_, three_in_one, reverse) DECLARE_TYPE(gl_unsigned_int_5_9_9_9_rev, ufloat32_, three_in_one, reverse) DECLARE_TYPE(gl_float_32_unsigned_int_24_8_rev, float32_stride32_, two_in_one, reverse)
#undef DECLARE_TYPE
The numeric aspect of every data type is one of the numeric type defined above, which has all its properties well defined. Then, the packing aspect can describe how colors components are packed in the source data (we can have a set of integers on 32bits but do we have one, two, three or four components packed in it?), then the order aspect describes how these components are ordered.
Then constraints can be written by querying these aspects (which is done in a generalized way, and using boilerplate code generated by the macros) and using MPL to generate boolean conditions:
template< typename DataType, typename GroupType > struct integral_check { typedef bm::and_< fnum::integral::is_integral< typename fcmn::get_numeric< GroupType >::type >, fnum::integral::is_not_integral< typename fcmn::get_numeric< DataType >::type > > group_is_integral_but_data_is_not; typedef bm::not_< group_is_integral_but_data_is_not > type;
static_assert(type::value, "GroupType is not compatible with DataType"); static_assert(type::value, ""); static_assert(type::value, " [3.7.2 Transfer of Pixel Rectangles]"); static_assert(type::value, " [3.9.3 Texture Image Specification]"); static_assert(bm::not_< group_is_integral_but_data_is_not >::value, " - GroupType is integral but DataType is not."); };
template< typename DataType, typename GroupType > struct component_check { typedef bm::and_< fcmn::component::is_depth_stencil< GroupType >, fcmn::packing::is_not_two_in_one< DataType > > group_is_depth_stencil_but_data_is_not_two_packed; typedef bm::not_< group_is_depth_stencil_but_data_is_not_two_packed > type;
static_assert(type::value, "GroupType is not compatible with DataType"); static_assert(type::value, ""); static_assert(type::value, " [3.7.2 Transfer of Pixel Rectangles]"); static_assert(type::value, " [3.9.3 Texture Image Specification]"); static_assert(bm::not_< group_is_depth_stencil_but_data_is_not_two_packed >::value, " - GroupType is depth_stencil but DataType is not two_in_one packed."); };
I'm still not really getting this. :( It *looks* to me like you're doing something equivalent to namespace numeric_tags { /*...*/ } namespace packing_tags { /*...*/ } namespace order_tags { /*...*/ } template< class T > struct gl_data_traits; template<> struct gl_data_traits< gl_unsigned_byte > { typedef numeric_tags::uint8_ numeric; typedef packing_tags::one_in_one packing; typedef order_tags::forward order; }; template<> struct gl_data_traits< gl_byte > { /*...*/ }; /* ...etc... */ I would not consider anything that I typed above to be particularly novel; it's basically the same pattern that, e.g., std::iterator_traits and std::numeric_limits have. I don't see really any functional difference between what I wrote and what you wrote. Clearly there's something "new" about your design that I think I'm missing :( - Jeff

I'm still not really getting this. :(
It *looks* to me like you're doing something equivalent to
namespace numeric_tags { /*...*/ } namespace packing_tags { /*...*/ } namespace order_tags { /*...*/ }
template< class T > struct gl_data_traits;
template<> struct gl_data_traits< gl_unsigned_byte > { typedef numeric_tags::uint8_ numeric; typedef packing_tags::one_in_one packing; typedef order_tags::forward order; }; template<> struct gl_data_traits< gl_byte > { /*...*/ }; /* ...etc... */
I would not consider anything that I typed above to be particularly novel; it's basically the same pattern that, e.g., std::iterator_traits and std::numeric_limits have. I don't see really any functional difference between what I wrote and what you wrote. Clearly there's something "new" about your design that I think I'm missing :(
Well, I'm not really saying it is new, I'm asking if it is. Maybe it isn't. I can see now with your example, that this is very close to the tags used in the iterator_traits for example (and the other traits if we consider value_type etc... as axes with not restricted values...). The idea is maybe that I generalize a little bit this mechanism, and especially in the way properties are defined and accessed. Starting from your example, the way to retrieve the aspects of data type is by accessing gl_data_traits< type >. But this traits type is valid for data types only as the gl_data_type structure has a fixed list of traits/tags that are not able to describe anything else than a gl_data. In my code, if I assume that a given type to have a numeric aspect, then I'm able to call get_numeric< Type >::type to retrieve this aspect, whatever type I provide. This is because I have a common mechanism to define and embed aspects in the types themselves. The type aspects are embedded in an "aspect" typedef in every type, which then is expected to contain a "numeric" member describing the numeric aspect of the type. This is not limited to gl_data type anymore and the same accessing mechanism can be used with any type that is expected to have some numeric properties. That's what I do in the constraint checking code I've pasted above: typedef bm::and_< fnum::integral::is_integral< typename fcmn::get_numeric< GroupType >::type >, fnum::integral::is_not_integral< typename fcmn::get_numeric< DataType >::type > > group_is_integral_but_data_is_not; Using an iterator_traits describing way, this would require to know what traits type to use with GroupType and which one to use with DataType to access the numeric properties of these types. On Thu, Feb 9, 2012 at 1:35 AM, Topher Cooper <topher@topherc.net> wrote:
A non-substantive comment:
I would suggest that you use a different name for this if it goes beyond purely personal use.
The reason is that there is a technology called Aspect Oriented Programming that is in widespread use and under active research and development. In general it is an extension to modularization but is usually seen as an extension to Object Oriented Programming. Many, but not all, of the concerns it addresses can be handled in C++ with generic programming, mix-ins and (for run time aspects) delegation.
How about Type Axises? Seems to capture what you consider distinctive to this approach.
I know about AOP, and I know Type Traits was a little bit misleading... but as I said, it's been chosen mostly by lack of any other nice name. I'm not very attached to it but I think it describes well what I want to represent. Type Axes... I'm not linking this as much. I was thinking of Type Facets, but facets is also already in use in the standard library. Actually isn't this description of types over some orthogonal properties quite close to AOP model? As far as I could remember, AOP is also about describing a program along orthogonal aspects (such as security, logging, etc...), right? -- Beren Minor

On 2/9/2012 2:54 AM, Beren Minor wrote:
On Thu, Feb 9, 2012 at 1:35 AM, Topher Cooper<topher@topherc.net> wrote:
The reason is that there is a technology called Aspect Oriented Programming that is in widespread use and under active research and development. In general it is an extension to modularization but is usually seen as an extension to Object Oriented Programming. Many, but not all, of the concerns it addresses can be handled in C++ with generic programming, mix-ins and (for run time aspects) delegation.
How about Type Axises? Seems to capture what you consider distinctive to this approach.
Actually isn't this description of types over some orthogonal properties quite close to AOP model? As far as I could remember, AOP is also about describing a program along orthogonal aspects (such as security, logging, etc...), right?
I've researched AOP but never used it -- and using is understanding -- so take this with the possibility of misunderstanding on my part but...
I think that the answer is ... yes and no. AOP does address program concepts orthoganal to the usual ones, but I think that what you are addressing is orthogonal to AOP as well. AOP is really about modules, not about types -- this can be misunderstood because OOP conflates type and module (not a criticism -- in fact, Trellis/Owl, an OO language I developed about the same time C++ was originally being developed, used the term "TypeModule" instead of class). Trait-ish things provide, essentially, information about a type that can then be used metaprogrammatically. The point of modules is that they provide isolation of their concepts. The problem with modules is that they provide isolation of their concepts. AOP grew out of a belief that this meant that any decision about strict modularization had to choose some things to represent as primary (what the modules represent) but that other things that are equally important, such as security, logging, etc. gets broken up -- duplicated or handled differently within each module, even when there are modules (e.g., a logging or a security library) to support those concepts ("aspects"). If you want to make changes to how one of these is handled, one needs to go into each module to make the changes. In most versions of AOP, however, one does not have to fully anticipate precisely what constitutes an Aspect in advance, and the changes may even be made at run time. Think of AOP (AOP advocates may scream at this, I'm not sure) as a framework for tools for editing the source, object or executable or running image across a whole program in parallel by describing to the tool how to identify what needs to be changed and how to change it without needing to deal with each instance. Perhaps you might think of this as a kind of highly sophisticated parameterized find-and-replace-all that operates on a level of abstraction beyond the text level (even when applied to source). Trait-ish concepts might be used as part of the vocabulary for describing these "cut-points" (identifiable places to make changes) but they aren't really the same thing. I'm not sure whether or not I agree with the AOP solution. It seems to me that there should be an extension to OO rather than development orthogonal to it that would handle these concerns in a more clean fashion, but I can't pretend to know just what that extension would be and I might be missing something in the concept that makes it less ad hoc, more elegant and concerned with abstractions that are truly beyond OO than appears to me. And, of course, my comment was concerned about confusion due to "aspect" having an understood technical meaning (for that matter, one in wider circulation than "traits" or "facets"), with the utility of that concept being irrelevant. Topher Topher

On 2/8/2012 5:44 PM, Beren Minor wrote:
Hi everyone,
I would like to introduce a project I'm working on and a mechanism that emerged from the projects needs and, as far as I could feel, may be useful in a general case. By lack of words (let say), I named this Type Aspects, as an analogy to Type Traits.
A non-substantive comment: I would suggest that you use a different name for this if it goes beyond purely personal use. The reason is that there is a technology called Aspect Oriented Programming that is in widespread use and under active research and development. In general it is an extension to modularization but is usually seen as an extension to Object Oriented Programming. Many, but not all, of the concerns it addresses can be handled in C++ with generic programming, mix-ins and (for run time aspects) delegation. How about Type Axises? Seems to capture what you consider distinctive to this approach. Topher
participants (3)
-
Beren Minor
-
Jeffrey Lee Hellrung, Jr.
-
Topher Cooper