Is there a reason, other than jargon, why this concept is called different names in different languages?
Do variants work different than tagged unions or type classes?
Some people told me that Haskell is superior in FP because of its type classes. Later I read TypeScript has tagged unions, which looked like type classes to me.
Then I looked into Reason and they wrote about variants and I also was reminded of type classes.
The tagged union/variant idea does not correspond to a Haskell type class. In fact it doesn't really even have a name in Haskell it's just the "sum type" aspect of a "algebraic data types" (which constitutes sums, products and functions, basically).
You can sort of consider them as open sum types which third parties can add additional summands to but that's probably not a very helpful way of thinking about them.
They’re known in other languages as interfaces, traits, or protocols: just a set of methods which can be implemented for different types.
Importantly, it’s possible to implement type classes for existing types in a separate declaration, as opposed to traditional OO languages where a list of implemented interfaces has to be declared upfront as part of the class declaration. Thus, for instance, a client of a library can declare its own type class and implement it for the library’s types. (Many languages other than Haskell support this too, though.)
They achieve the same result, but in a quite different way. I see them as decoupling the "implements X" piece of a traditional OO class in a language that has interfaces from the class itself.
If you're familiar with Scala, then type classes in Haskell are pretty close to traits in Scala (but not implicit conversions, which can make use of traits, but are an orthogonal language feature). They're also close to Rust's traits, it you're familiar with Rust.
Tagged unions are an implementation of variants using a discriminating tag and a union type.
Variants are sometimes called sum types to emphasize the duality with product types (tuples).
Type classes are a completely unrelated concept, as others have already commented. They are more like vtables in C++: providing functions to operate on an otherwise opaque object.
I don't want to be that guy but there is pretty solid explanation of vtables if you do a web search.
Some of the language also derives from either more traditional set theory (union, disjoint union, discriminated union) or from category theory (sum, coproduct) even though all of these things might mean the same concept in this particular case. Category theory is comparatively "new" (barely 70 years old) and so older works and languages tend not to use it.
nothing wrong with being that guy but at the same time there's also nothing wrong with admitting you don't know what something is before you go and look it up. i don't want to be that guy but the poster you responded to didn't ask what they were, only stated that they don't know what they are. im sure if they were curious enough and thought it relevant enough to their knowledge base they would go look it up of their own accord.
also, another point I'd really like to bring up is, while in general im comfortable with doing research and learning something from resources on the web, some people are less confident in their abilities to understand something and would prefer to ask another human being to explain it so they can open up the possibility of discussion if they don't particularly understand the verbiage or concepts contained within the answer; it's much easier to ask a person a question than to ask an article or a tutorial one when said resource may not even be actively maintained or monitored anymore and social interaction may have potentially died down on it.
im not trying to really complain as I do believe everyone should really take the time to do their own research and come to their own conclusions, but having someone they can ask is useful in and of itself in the same way having a professor in college you can ask to clarify is being useful. if you don't have another individual that is knowledgeable about the concepts you're unsure about your perception of to potentially confirm or deny your conclusions, then you're basically operating on unknown unknowns, and as far as I understand, in intellectual practice, it is the desire of the practitioner to eliminate any possible unknown unknowns through whatever means necessary and account for all possibilities and deviations.
Variants, sum types and type classes are all different notions. In PureScript variants (https://github.com/natefaubion/purescript-variant) are used to have a homogeneous sum type that mixes different types, sum types is a single type with multiple constructors. Type classes others already explained.
variant_cast is just garbage. It relies on struct layout, and if anything has wider alignment than ptrdiff_t, it's broken.
For example, a double in 32-bit PowerPC has 64-bit alignment but ptrdiff_t is 32 bits, so the pointer is wrong. Maybe x64 you get away with it for most types (not all!), but can you raise your right hand and solemnly swear that you'll never ever want to port to a non-x64 system?
Additionally, all the explicit casting means that this has basically no type safety at all. I can a.tag = tag(SomeThree) even though SomeThree is not a member of a's type, and then I will be able to successfully let b = as(&a, SomeThree) and boom! I've just scribbled over memory without so much as a peep from the compiler. What is the of having tagged unions (a TYPE SAFETY FEATURE) if your tagged union implementation is LESS type safe than untagged unions?
This last point is actually a bit subtle… accessing the wrong member of a union is probably not what you want to do most of the time, but the language in the C standard is quite clear (see DR #257, language was clarified in C99 but present in earlier versions) and accessing the wrong union member just gives you whatever you stored in a union reinterpreted as whatever you read out of it. But this tagged union implementation lets you access a union member which doesn't even exist.
Let's not try to implement new language features on top of C with macros. It never ends well. Either figure out a way to live with a little boilerplate or use a different language, those are the only sane options.
It's true that this implementation is wrong, but I disagree that it's a bad idea. Certainly it's possible to do correctly in various ways, e.g. by having the macro pass offsetof(Some, the_union) (but the existing code doesn't name the union and there would have to be consideration for multiple variant types).
"Certainly it's possible to do correctly in various ways"
Not very convincing. At the very least, you'd need to pass in the type name as a macro parameter, use non-portable GCC extensions, or define some additional boilerplate for each union type. We can't just say "offsetof" and handwave the actual macro definition here.
The traditional solution, to define an enum type, works reasonably. You can certainly get silent type errors from typos but perhaps we can tolerate the risk. It's not worse than, say, qsort in terms of type safety.
The options I alluded to above are due to the fact that in order to allow the compiler to type-check for us, we need to keep the type of "x" passed in to "as()". You need to check the tag and either abort or return the correct union member. So "x" appears twice, but that's a bad idea for a macro. So you either use the GCC extension which allows statements as expressions, or you dispatch to a boilerplate function for each union type. I can't think of a different way to do things.
Sorry, I was in a hurry when I wrote that comment :)
The easiest way is to take advantage of the fact that a pointer to a union can be casted to a pointer to one of its members (C11 6.7.2.1.16), or in other words that all the possible variants are at the same offset. So we can do:
typedef struct Some variantof_SomeOne;
#define as(x, T) ((struct T *)variant_cast(x, tag(T), \
offsetof(variantof_##T, u)))
where the definition of Some would have to be changed to give the union field a name, and variant_cast would add the offset given as an argument.
An alternative would be one of the options you mentioned, a boilerplate function. But it doesn't have to be 'real' boilerplate: it's possible to use macros to automate the definition of such functions. Given suitable macros, you can write something like:
tag() just needs to take 'a' as another parameter and do a static_assert.
> It relies on struct layout
As in "ptrdiff_t needs to be the first member in a struct"? Oh, the horror. It's C, a language that generally expects you to know what you are doing.
> all the explicit casting means that this has basically no type safety at all
Duh. See above :)
Generally speaking, you are missing a point here. This sort of contraption comes handy when the code needs to add a _bit_ more of invariant checking, but without going over the top. In comparison with a plain union this will help catching the misuse. Granted, it needs to be used correctly itself.
> As in "ptrdiff_t needs to be the first member in a struct"? Oh, the horror. It's C, a language that generally expects you to know what you are doing.
Speaking of garbage, that's some really needless posturing. The comment was about the position of the second element in the structure, I suppose that this wasn't spelled out in the comment but everyone here knows that the first member of a struct is at the beginning.
God forbid you would store __mm128i here, although generally that's an awful idea, it's not unreasonable to want to force 16-byte alignment or the like.
> tag() just needs to take 'a' as another parameter and do a static_assert.
Show me. I'm honestly curious what it would look like.
> Generally speaking, you are missing a point here. This sort of contraption comes handy when the code needs to add a _bit_ more of invariant checking, but without going over the top. In comparison with a plain union this will help catching the misuse. Granted, it needs to be used correctly itself.
> The comment was about the position of the second element in the structure, I suppose that this wasn't spelled out in the comment but everyone here knows that the first member of a struct is at the beginning.
I'm flattered that you want to talk about me, but I'd rather talk about the code.
> An exercise left to the reader.
That's really the only interesting part, isn't it? All the other stuff is just minor bugfixes / bikeshedding. I said "show me" the version of tag() that does the static_assert, and "show me" a version that adds invariant checking (that the cast is possible). Neither of these things have been shown. I'm not claiming it's impossible, just that subjectively, this version is worse than doing things the boring way.
If you don't understand that tag_union(&a, SomeThree) throwing a compile time error is functionally equivalent to static_assert(), then I see no point in continuing this exchange. Ditto for the invariant checking - OP's original code already covered that with its assert(). In fact, that assert() is what this whole thing is about.
All points in your opening "garbage" comment were trivial nitpicks, which would've been fine if you didn't decide to express them by trashing other person's code in an overly confident manner. I don't appreciate that, just as I don't appreciate you trying to wiggle out from this exchange on technicalities.
Yes, you're right, tag_union does throw a compile time error, I didn't read that correctly. By your earlier comment I was primed for a `_Static_assert` and when I didn't see that. I thought it had been left out entirely rather than reimplemented in a different manner.
On the subject of tone--I honestly believe that the original code has severe subjective design flaws and objective implementation errors, and since the post was gathering upvotes but no critique I was worried that people believed that the code was correct. There are still some nitpicks about your new version, but these are fairly minor in my view (the use of reserved identifiers is pretty tedious and easily avoided). The remaining non-nitpick, what I consider to be a show-stopper, is that you can't switch on the tag. 90% of the time when I'm using a tagged union, I'm doing switch (evt->type). While we can substitute a chain of if/else, I'm not fond of the ergonomics and I would prefer a traditional enum. This is a subjective trade-off, however.
Your responses would have been fine if they were criticisms of my argument or of the tone I took. Perhaps I crossed the line, and you called me on it? But these comments are unacceptable ad hominems:
> Oh, the horror. It's C, a language that generally expects you to know what you are doing.
> It relies on struct layout, and if anything has wider alignment than ptrdiff_t, it's broken.
I think you're wrong about this. I'm pretty certain a pointer to a struct is always equivilent to a pointer to the first member, except for the type. I went and looked it up in C11 (6.7.2.1 point 15):
> A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
Edit: A duh, the + sizeof(ptrdiff_t) is the part that's wrong. Yeah, agreed.
Yes - this is what allows you to some semblance of struct inheritance. You can embed a structure as the first member in the 'derived' structure and then up and down cast as appropriate.
> Let's not try to implement new language features on top of C with macros.
Thank you for that. This happens though, in fact I've done it because the CEO of a company I worked for did not want to use anything but C and the project had a bunch of requirements that were very hard to meet with C. It's surprising how far you can get with that approach but in the end it is simply the wrong way to approach the problem. The right tool for the job is the best way to start any job.
Essentially it came down to re-implementing half of BEAM and Erlang OTP in C. I would have much rather used Erlang. Worst case of NIH that I've seen to date.
I'm not a C programmer, but isn't there some directive you can add to a union to tightly pack it, or specify member memory locations or something? So that then you could guarantee that for the Some struct, `ptrdiff_t tag;` starts at offset 0 and the union starts at offset `sizeof(ptrdiff_t)`?
39 comments
[ 5.0 ms ] story [ 88.6 ms ] threadDo variants work different than tagged unions or type classes?
Some people told me that Haskell is superior in FP because of its type classes. Later I read TypeScript has tagged unions, which looked like type classes to me.
Then I looked into Reason and they wrote about variants and I also was reminded of type classes.
I thought that type classes are sum types also.
Care to explain what type classes are then? :)
Importantly, it’s possible to implement type classes for existing types in a separate declaration, as opposed to traditional OO languages where a list of implemented interfaces has to be declared upfront as part of the class declaration. Thus, for instance, a client of a library can declare its own type class and implement it for the library’s types. (Many languages other than Haskell support this too, though.)
Variants are sometimes called sum types to emphasize the duality with product types (tuples).
Type classes are a completely unrelated concept, as others have already commented. They are more like vtables in C++: providing functions to operate on an otherwise opaque object.
I thought they were the Haskell equivalent.
I also don't know what vtables are, haha.
Some of the language also derives from either more traditional set theory (union, disjoint union, discriminated union) or from category theory (sum, coproduct) even though all of these things might mean the same concept in this particular case. Category theory is comparatively "new" (barely 70 years old) and so older works and languages tend not to use it.
also, another point I'd really like to bring up is, while in general im comfortable with doing research and learning something from resources on the web, some people are less confident in their abilities to understand something and would prefer to ask another human being to explain it so they can open up the possibility of discussion if they don't particularly understand the verbiage or concepts contained within the answer; it's much easier to ask a person a question than to ask an article or a tutorial one when said resource may not even be actively maintained or monitored anymore and social interaction may have potentially died down on it.
im not trying to really complain as I do believe everyone should really take the time to do their own research and come to their own conclusions, but having someone they can ask is useful in and of itself in the same way having a professor in college you can ask to clarify is being useful. if you don't have another individual that is knowledgeable about the concepts you're unsure about your perception of to potentially confirm or deny your conclusions, then you're basically operating on unknown unknowns, and as far as I understand, in intellectual practice, it is the desire of the practitioner to eliminate any possible unknown unknowns through whatever means necessary and account for all possibilities and deviations.
but yeah, Google first then ask questions!
variant_cast is just garbage. It relies on struct layout, and if anything has wider alignment than ptrdiff_t, it's broken. For example, a double in 32-bit PowerPC has 64-bit alignment but ptrdiff_t is 32 bits, so the pointer is wrong. Maybe x64 you get away with it for most types (not all!), but can you raise your right hand and solemnly swear that you'll never ever want to port to a non-x64 system?
Additionally, all the explicit casting means that this has basically no type safety at all. I can a.tag = tag(SomeThree) even though SomeThree is not a member of a's type, and then I will be able to successfully let b = as(&a, SomeThree) and boom! I've just scribbled over memory without so much as a peep from the compiler. What is the of having tagged unions (a TYPE SAFETY FEATURE) if your tagged union implementation is LESS type safe than untagged unions?
This last point is actually a bit subtle… accessing the wrong member of a union is probably not what you want to do most of the time, but the language in the C standard is quite clear (see DR #257, language was clarified in C99 but present in earlier versions) and accessing the wrong union member just gives you whatever you stored in a union reinterpreted as whatever you read out of it. But this tagged union implementation lets you access a union member which doesn't even exist.
Let's not try to implement new language features on top of C with macros. It never ends well. Either figure out a way to live with a little boilerplate or use a different language, those are the only sane options.
Not very convincing. At the very least, you'd need to pass in the type name as a macro parameter, use non-portable GCC extensions, or define some additional boilerplate for each union type. We can't just say "offsetof" and handwave the actual macro definition here.
The traditional solution, to define an enum type, works reasonably. You can certainly get silent type errors from typos but perhaps we can tolerate the risk. It's not worse than, say, qsort in terms of type safety.
The options I alluded to above are due to the fact that in order to allow the compiler to type-check for us, we need to keep the type of "x" passed in to "as()". You need to check the tag and either abort or return the correct union member. So "x" appears twice, but that's a bad idea for a macro. So you either use the GCC extension which allows statements as expressions, or you dispatch to a boilerplate function for each union type. I can't think of a different way to do things.
Sorry, I was in a hurry when I wrote that comment :)
The easiest way is to take advantage of the fact that a pointer to a union can be casted to a pointer to one of its members (C11 6.7.2.1.16), or in other words that all the possible variants are at the same offset. So we can do:
where the definition of Some would have to be changed to give the union field a name, and variant_cast would add the offset given as an argument.An alternative would be one of the options you mentioned, a boilerplate function. But it doesn't have to be 'real' boilerplate: it's possible to use macros to automate the definition of such functions. Given suitable macros, you can write something like:
and get as much boilerplate as you want, like:All your nitpicks are fairly easy to address.
> I can a.tag = tag(SomeThree)tag() just needs to take 'a' as another parameter and do a static_assert.
> It relies on struct layout
As in "ptrdiff_t needs to be the first member in a struct"? Oh, the horror. It's C, a language that generally expects you to know what you are doing.
> all the explicit casting means that this has basically no type safety at all
Duh. See above :)
Generally speaking, you are missing a point here. This sort of contraption comes handy when the code needs to add a _bit_ more of invariant checking, but without going over the top. In comparison with a plain union this will help catching the misuse. Granted, it needs to be used correctly itself.
Speaking of garbage, that's some really needless posturing. The comment was about the position of the second element in the structure, I suppose that this wasn't spelled out in the comment but everyone here knows that the first member of a struct is at the beginning.
God forbid you would store __mm128i here, although generally that's an awful idea, it's not unreasonable to want to force 16-byte alignment or the like.
> tag() just needs to take 'a' as another parameter and do a static_assert.
Show me. I'm honestly curious what it would look like.
> Generally speaking, you are missing a point here. This sort of contraption comes handy when the code needs to add a _bit_ more of invariant checking, but without going over the top. In comparison with a plain union this will help catching the misuse. Granted, it needs to be used correctly itself.
Show me a version that adds invariant checking.
Wit beyond measure, fascinating.
Shall I sprinkle some comments or is it OK this way?> Show me.
> Show me a version that adds invariant checking.An exercise left to the reader.
I'm flattered that you want to talk about me, but I'd rather talk about the code.
> An exercise left to the reader.
That's really the only interesting part, isn't it? All the other stuff is just minor bugfixes / bikeshedding. I said "show me" the version of tag() that does the static_assert, and "show me" a version that adds invariant checking (that the cast is possible). Neither of these things have been shown. I'm not claiming it's impossible, just that subjectively, this version is worse than doing things the boring way.
All points in your opening "garbage" comment were trivial nitpicks, which would've been fine if you didn't decide to express them by trashing other person's code in an overly confident manner. I don't appreciate that, just as I don't appreciate you trying to wiggle out from this exchange on technicalities.
On the subject of tone--I honestly believe that the original code has severe subjective design flaws and objective implementation errors, and since the post was gathering upvotes but no critique I was worried that people believed that the code was correct. There are still some nitpicks about your new version, but these are fairly minor in my view (the use of reserved identifiers is pretty tedious and easily avoided). The remaining non-nitpick, what I consider to be a show-stopper, is that you can't switch on the tag. 90% of the time when I'm using a tagged union, I'm doing switch (evt->type). While we can substitute a chain of if/else, I'm not fond of the ergonomics and I would prefer a traditional enum. This is a subjective trade-off, however.
Your responses would have been fine if they were criticisms of my argument or of the tone I took. Perhaps I crossed the line, and you called me on it? But these comments are unacceptable ad hominems:
> Oh, the horror. It's C, a language that generally expects you to know what you are doing.
> Wit beyond measure, fascinating.
I think you're wrong about this. I'm pretty certain a pointer to a struct is always equivilent to a pointer to the first member, except for the type. I went and looked it up in C11 (6.7.2.1 point 15):
> A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
Edit: A duh, the + sizeof(ptrdiff_t) is the part that's wrong. Yeah, agreed.
Thank you for that. This happens though, in fact I've done it because the CEO of a company I worked for did not want to use anything but C and the project had a bunch of requirements that were very hard to meet with C. It's surprising how far you can get with that approach but in the end it is simply the wrong way to approach the problem. The right tool for the job is the best way to start any job.
Can you shed any light on that?
The compiler is allowed to insert padding for a good reason, and packing is rarely the right solution for anything.